PS
All projects
In Progress2026

Nestora

A society management platform for the people who actually live there

Residential society operations in one product — visitors, maintenance billing, complaints, notices and amenities — across five roles. Flutter app, NestJS API over 15 domain modules, a 22-model Postgres schema, and a separate super-admin service.

FlutterNestJSPostgreSQLPrismaFirebase AuthJWTDart

15

Domain modules

22

Prisma models

34

Migrations

5

Roles

Overview

Nestora is a management platform for residential societies. It covers the things a housing society actually runs on day to day — who came through the gate, who has paid their maintenance, what the committee has announced, which complaint is still open, and who booked the clubhouse on Saturday.

The distinguishing feature is that it isn't one product with an admin mode bolted on. There are five genuinely different roles — super admin, society admin, security guard, resident and maintenance staff — and each gets its own shell in the app, with the API enforcing what that role can see rather than the UI merely hiding it.

It runs as a Flutter client against a NestJS API over PostgreSQL, plus a separate admin service that owns platform-level concerns: onboarding societies, subscription plans, feature flags and audit.

Problem

Society management runs on WhatsApp groups, a paper register at the gate, and a treasurer's spreadsheet. Each of those fails in a specific way: the group scrolls past anything important, the register can't be searched or audited, and the spreadsheet is one person's laptop away from being lost.

Maintenance billing is the worst of it. Rent and monthly maintenance recur, one-off charges don't, and residents move in mid-month — so 'what does flat A-402 owe' becomes a question nobody can answer consistently, and the answer changes depending on who you ask.

Security has the opposite problem: too much data, none of it usable. A gate register records every visitor and can tell you nothing — you can't check whether a delivery was expected, whether a guest is pre-approved, or who is still inside the premises right now.

The existing products mostly sell to the builder or the management company, so they're designed for the committee's reporting rather than the resident's day. The person who actually needs to pay a bill or approve a guest is the one the software serves least.

Solution

Nestora models the society itself — towers, flats, residents — and hangs every other feature off that structure, so a bill, a complaint and a visitor entry all resolve to a specific flat rather than to a free-text name.

Billing is the part that got the most design attention. Bills are typed as recurring rent, recurring maintenance or a one-off charge, and the recurring ones are generated by a lazy backfill on read rather than a cron job that can silently miss a month. A resident's due date is anchored to their move-in day, deletion is soft so the backfill can't resurrect a removed bill, and bills paid in one tap share a timestamp so a payment groups into a single history entry instead of scattering.

The gate is split into three concerns rather than one register: walk-in visitors carrying an inside/exited state, pre-approved visitors a resident authorises in advance, and deliveries. That makes 'who is inside right now' a query rather than a manual scan.

Around that sit complaints with a real status lifecycle, a notice board, amenity booking with an approval step, support tickets, and in-app notifications. Residents can generate a PDF receipt for a payment on-device and save or share it — the receipt is produced by the client, so it works without a document service behind it.

Platform concerns deliberately live in a second service with its own schema and its own auth middleware. Onboarding a society, moving it between subscription plans, flipping a feature flag or reading the audit log are not things the resident-facing API should be able to do at all.

Features

Five roles, five shells

Super admin, society admin, security guard, resident and maintenance staff. Role is resolved at sign-in and routes to a different app shell; the API guards each endpoint independently rather than trusting the client's choice.

Recurring maintenance billing

Rent and monthly maintenance are generated by a lazy backfill on read, deduplicated against existing rows, and anchored to each resident's move-in day. One-off charges carry their own title so several can coexist in the same month.

Payments grouped into history

Bills settled together share an exact paid-at timestamp, which is what turns several rows into one readable payment in the resident's history instead of a list of fragments.

On-device PDF receipts

The Flutter client builds a receipt for a payment, saves it straight to the device and opens it — no document service, no server round trip, and it works offline once the payment is recorded.

Gate management in three parts

Walk-in visitors with an inside/exited state, pre-approvals a resident creates ahead of time, and deliveries — separated so the security panel can answer who is currently on the premises.

Complaints with a lifecycle

Open, in progress and resolved are real states on the record rather than a label someone types, so a committee can see what's actually outstanding.

Notice board and notifications

Society notices alongside per-user in-app notifications and platform-wide announcements, kept as separate models because they have different audiences and lifetimes.

Amenity booking with approval

Residents request a slot for a shared amenity; the booking sits pending until an admin approves or rejects it, so the clubhouse can't be double-booked by whoever asked first.

Societies with or without towers

A flag on the society decides whether flats are numbered A101 or plain 101. It's fixed at creation on purpose — flipping it later would renumber every flat in the building.

Subscription plans

Free, monthly and yearly plans sit on the society record and are administered from the super-admin service, keeping the commercial layer out of the resident-facing API.

Audit log and login events

Privileged actions and sign-ins are written to their own records, so an operations question has an answer that doesn't depend on someone remembering.

Feature flags and remote config

App config, feature flags and a minimum-version check let a build be steered after release — including forcing an upgrade when an API change isn't backward compatible.

Tech Stack

Mobile

  • Flutter
  • Dart
  • dio
  • shared_preferences

Device

  • image_picker
  • flutter_svg
  • pdf
  • file_saver
  • open_filex

API

  • NestJS
  • TypeScript
  • class-validator
  • class-transformer
  • @nestjs/config

Identity

  • Firebase Auth
  • firebase-admin
  • Passport JWT
  • bcrypt

Data

  • PostgreSQL
  • Prisma ORM

Admin

  • NestJS (separate service)
  • Prisma
  • admin auth middleware

Architecture

One Flutter client, two backend services, two schemas. The resident-facing API owns everything a society does day to day; a second admin service owns the platform itself. Splitting them means the app's API has no code path that can create a society or read another society's audit trail.

  1. Flutter client

    A single app carrying four in-app shells — resident, society admin, security guard and maintenance — chosen by the role on the authenticated session. A thin dio-based API client and a persisted session keep auth out of the screens.

    Flutterdioshared_preferences
  2. Nestora API

    NestJS with one module per domain: societies, residents, bills, complaints, notices, amenities, staff, support, users, uploads, notifications, app-version, content, and a gate module that splits into visitors, pre-approvals and deliveries.

    NestJSclass-validator
  3. Auth layer

    Firebase handles phone verification; the API mints its own JWT through a Passport strategy and guards routes by role, with bcrypt for the credentials it stores itself. Identity is verified once and carried structurally from there.

    Firebase AuthPassport JWTbcrypt
  4. PostgreSQL + Prisma

    22 models and 8 enums evolved across 34 incremental migrations. Money is stored as a fixed-precision decimal rather than a float, and deletion is soft where a hard delete would fight the recurring-bill backfill.

    PostgreSQLPrisma
  5. Super-admin service

    A separate NestJS application with its own schema and admin auth middleware, covering society onboarding, subscription plans, feature flags, platform settings, content pages and the audit log.

    NestJSPrisma

Folder Structure

nestora/lib/
Flutter client
├── api/
api_client, auth_api, firebase_auth_service, session
├── screens/resident/
Bills, complaints, notices, amenities, visitors
├── screens/society_admin/
Society, flats, residents, billing, approvals
├── screens/security_guard/
Gate entry, pre-approvals, deliveries
├── screens/maintenance/
Staff-facing work views
├── models/
Typed models — bill, visitor, complaint, amenity, notice
├── theme/, util/, utils/
Design tokens and shared helpers
└── main.dart, app_version.dart
Entry point and minimum-version gate
nestora/backend/src/
Resident-facing NestJS API
├── auth/
Guards, strategies, decorators, firebase-admin service
├── societies/ residents/ staff/
The structural core: society, tower, flat
├── bills/
Recurring generation, soft delete, payment grouping
├── gate/
visitors, pre-approved, deliveries
├── complaints/ notices/ amenities/
Day-to-day society operations
├── support/ notifications/ content/
Tickets, in-app messages, pages
└── uploads/ app-version/ common/
Files, version gate, shared utilities
nestora/backend/prisma/
schema.prisma + 34 migrations
nestora_admin/src/admin/
Super-admin controller + auth middleware

Database Design

22 models and 8 enums in PostgreSQL, grown across 34 migrations. The schema is organised around the physical building — society, tower, flat, resident — because almost every question the product answers is really a question about a flat. Money is decimal, not float; removal is soft where a recurring generator would otherwise recreate the row.

Society

The tenant boundary. Carries the recurring maintenance amount and the month it applies from, and a flag for buildings that have no towers — fixed at creation because changing it would renumber every flat.

  • name
  • address
  • city / state
  • logoUrl
  • hasTowers
  • monthlyMaintenance
  • subscriptionPlan

Tower / Flat

The physical structure every other record resolves to. Flats are numbered per tower, or flat-only for single-building societies.

  • societyId
  • letter
  • number
  • flats[]

Resident

A person's occupancy of a flat, typed as owner or tenant. Move-in date is what anchors their monthly due date.

  • userId
  • flatId
  • type (OWNER | TENANT)
  • moveInDate
  • rent

Bill

One charge against one flat for one period. Typed as recurring rent, recurring maintenance or a one-off charge; soft-deleted so the backfill's dedup can't resurrect it; paid-at shared across bills settled together.

  • flatId
  • period
  • amount (Decimal)
  • kind
  • title
  • dueDate
  • paidAt
  • deletedAt

Visitor / PreApprovedVisitor / Delivery

Three separate gate records rather than one register. Visitors carry an inside/exited state so the security panel can answer who is currently on the premises.

  • societyId
  • flatId
  • status (INSIDE | EXITED)
  • expectedAt
  • photo

Complaint

A resident-raised issue with a real status lifecycle — open, in progress, resolved — rather than a free-text label.

  • flatId
  • status
  • category
  • description
  • createdAt

Amenity / AmenityBooking

Shared facilities and the requests against them. Bookings sit pending until approved or rejected, which is what stops a double booking.

  • societyId
  • slot
  • status (PENDING | APPROVED | REJECTED)

AuditLog / LoginEvent

Privileged actions and sign-ins recorded as their own rows, so operational questions have an answer independent of anyone's memory.

  • actorId
  • action
  • target
  • createdAt

AppConfig / FeatureFlag / PlatformSettings

Remote configuration owned by the admin service — feature flags, platform settings, and a minimum app version that can force an upgrade when an API change isn't backward compatible.

  • key
  • value
  • enabled
  • minVersion

API Flow

What happens when a resident pays their outstanding maintenance.

  1. Resident opens the bills screen

    The client requests bills for their flat. The API runs the recurring backfill first — generating any rent and maintenance rows that are due but not yet materialised, deduplicated against what already exists.

  2. The API returns a settled list

    Soft-deleted rows are excluded, so a bill an admin removed doesn't reappear even though the generator would otherwise consider it missing.

  3. Resident selects and pays

    Several bills can be settled in one action. The API stamps every one of them with the same paid-at timestamp.

  4. History groups by that timestamp

    Because the timestamp is shared, the payment reads as a single entry in history rather than as one row per charge.

  5. The client builds a receipt

    A PDF is generated on-device from the payment, saved to the file system and opened — no document service in the path.

Challenges

Making recurring bills idempotent

A cron job that generates monthly bills fails in two directions: miss a run and a month is silently absent, run twice and everyone is billed double. Generating lazily on read and deduplicating against existing rows made the operation idempotent — but that only works if deletion is soft, because a hard-deleted bill looks exactly like a missing one to the generator.

Turning several bills into one payment

Residents pay everything outstanding in one tap, but the data model stores one row per charge. Grouping them in the UI by date was fragile. Stamping every bill settled in a single action with the identical timestamp made the grouping a property of the data rather than a heuristic in the client.

Five roles without five apps

Each role needs a genuinely different app, not a different menu. Resolving the role once at sign-in and routing to separate shells kept the screens simple, but it made it tempting to treat the client's role as authoritative — so every endpoint guards independently, and the shell is only a convenience.

Deciding what the admin service owns

Onboarding a society and reading an audit log could have been endpoints on the same API behind a role check. Splitting them into a separate service with its own schema means the resident-facing API has no code path to those actions at all, which is a stronger guarantee than a guard that could be misconfigured.

Societies that don't have towers

A single-building society has no A/B/C wings, and forcing one made flat numbers read wrong. Modelling it as a flag with a single empty-lettered tower kept one code path for both shapes — at the cost of making the flag immutable, since flipping it would renumber every flat.

Learnings

Idempotent generation beats scheduled generation. A job that can be run twice safely needs no monitoring; a job that can't needs alerting, retries and someone to answer the alert.

Soft delete isn't a preference here, it's a requirement — a generator that recreates missing rows can't tell 'removed' from 'never existed' unless the row stays.

Grouping belongs in the data. A shared timestamp made payment history correct everywhere at once, where a client-side heuristic would have needed reimplementing per screen.

Money is decimal. Storing amounts as floats is a bug that shows up as a rounding complaint months later, in production, from a resident.

Splitting a service is a cheap way to make a security boundary structural instead of conditional.

A minimum-version gate is worth building before you need it — the first backward-incompatible API change is the wrong time to discover you can't force an upgrade.

Future Improvements

Online payment collection, so a settled bill and a received payment are the same event rather than two.

Push notifications for due bills, approvals and gate events — the notification models exist, the delivery channel doesn't yet.

A committee-facing reporting view over collection rates and outstanding dues per tower.

Offline-first reads on the resident shell, so bills and notices are legible without a connection.

Automated tests around the recurring-bill generator specifically — it's the part where a regression is both most likely and least visible.

A web resident portal, for people who would rather not install an app for four interactions a year.