When you build software that maps to physical real estate—where assigning the wrong flat to the wrong person has legal and social consequences—getting the architecture right isn't just a technical preference; it's a product requirement. This post breaks down the backend architecture of Vaastio — a society platform for Indian residential communities. The stakes are high, and bad architecture doesn't announce itself on day one. It shows up six months later when you're trying to add a feature and realize everything is tangled together.
This post isn't a tutorial. It's a look at the core system design, the tough architectural decisions, and more importantly, "why" these decisions were made to ensure the platform starts with the builder and scales effortlessly with the society.
The Stack, And Why It's Boring
The backend runs on Node with Express, TypeScript, PostgreSQL, and Prisma. The mobile app uses React Native with Expo. The repository is managed with Turborepo. Nothing exotic.
That's deliberate. When you're building a product for Indian societies where data integrity is everything, where an admin accidentally assigning the wrong flat to the wrong person has real consequences, boring technology is a feature. It allows the focus to remain on product decisions instead of debugging framework quirks.
The underlying philosophy: Choose the technology that solves the problem. Save creativity for product decisions.

Identity: Separating the Person from the Account
One of the most impactful early architectural decisions was separating Person from User.
A User is an app account. It has a phone number, a password (well, OTP), and a token version. A Person is a real-world human being with a name, a flat, and a history in the society. The two are linked, but they're not the same thing.
Why does this matter? Because in societies, people change phones, share accounts with family members, and sometimes lose access to their number. The owner has an account, but their elderly parents living with them might not. Their domestic worker comes daily and doesn't need the app at all.
DECISION: Person ≠ User
- Person = real world identity.
- User = app account.
Person.userIdis nullable; a person can exist in the system without ever having the app.
Why it matters: Deactivating a member removes app access. It doesn't erase them from history. An owner who stops using the app is still an owner. A tenant who moves out is no longer active, but their past occupancy still exists in the records.
The Property Layer: The Self-Referential Tree
How do you model building structures when Client A has single units, Client B has Towers → Units, and Client C has Phases → Buildings → Wings → Floors → Units?
Instead of creating fixed tables for towers, floors, and units—which require a schema migration every time a new builder signs up with a weird layout—the system uses a single property_nodes table.


Each row simply points to its parent. This self-referential tree can handle any depth and any physical layout without a single schema change. A 20-flat building? All 20 units point to the root society node. A massive township? Build the tree as deep as you need.

Multi-tenancy: Hard Lines Between Societies
Because Vaastio is a multi-tenant SaaS platform, ensuring that residents from one society never see data from another is mission-critical.
Every API request carries an organization ID in the JWT token. Every database query scopes to that orgId. Every route goes through a tenant context middleware that verifies the token's orgId matches the requested society.
This sounds obvious. It's not. Most apps handle this informally—a WHERE clause here, a check there. The architecture makes this structural. There is no way to accidentally query another society's data because the system prevents it, not just the developer's attention.

Permissions: Database Driven, Not Hardcoded
Early on, the decision was made that permissions wouldn't be hardcoded if (role === 'admin') strings scattered across route files. They live in the database—a Permissions table, a Roles table, a RolePermissions join table. When a user authenticates, their full permission set is loaded and embedded in their JWT.
The practical benefit: adding a new permission to a role is a seed file change, not a code change. No deployment required. No searching through files to find where 'admin can do X' is checked.
Permissions are granular and descriptive: announcement.create, unit.assign, complaint.view_all. A resident can see their own flat (unit.view_own). An admin can see every flat. The same endpoint handles both; it checks which permission the caller has and scopes the response accordingly. It isn't complicated technology, but putting it in place early prevented a complete rewrite of the permission logic later.
Notifications: Event Driven, Never Coupled
Many applications wire notifications directly into route handlers. A complaint is created, so the route calls sendPushNotification(adminId, 'New complaint'). It works until you need to change who gets notified, or add a new notification type, or handle failures gracefully. Then everything is tangled.
Vaastio uses an event-driven notification system instead. Routes emit events. A dispatcher listens. A rules file decides who gets what message.

Adding a new notification type takes three lines across three files. Changing who receives a notification is one function change in rules.ts. The route handler never changes.
DECISION: Notifications are a side effect, not part of the response Routes emit events. Dispatchers handle delivery. A failure in notification never returns an error to the user.
Why it matters: Users shouldn't see errors because Expo's servers had a blip. The complaint was created. That's what matters. The notification is best-effort.
Data: Nothing Is Ever Deleted
The system doesn't hard delete anything. Members are deactivated, not removed. Occupancies get an occupiedUntil date. Ownerships get an ownedUntil date. Complaints get a resolvedAt. Everything is preserved.
In Indian society management, disputes about who owned a flat three years ago are real. "Who raised that complaint in 2023?" could be a real question. The answer should always be accessible. Soft deletes cost nothing at this scale and prevent a category of support problems entirely. Proper indexing ensures these soft-deleted rows don't impact query performance for active records.
The Trust Model: What Admins Can and Can't Do
This is where architecture meets ethics. An admin in this system has significant power—they can assign flats, deactivate members, and manage complaints. The core question became: how much should the system trust admins?
The V1 approach: trust them, but make everything visible. Every change is audit-logged with who made it and when. Rogue changes are visible to affected residents in their "My Home" screen. If an admin silently adds themselves as a co-owner of your flat, you'll see it immediately.
The system blocks the most terrible abuse (an admin cannot add themselves as the owner of a flat that already has a different owner). But it doesn't block all self-assignment, because a builder legitimately needs to assign their own unsold flats to themselves and there's nobody above them to do it.
DECISION: V1 Trust Model - Visibility over Restriction
- Admin has authority.
- Audit log tracks everything.
- Affected parties can see changes.
- Obvious abuse is blocked.
Why not full restriction: Over-restricting creates a support burden and friction for legitimate use cases. Under-restricting with full visibility is the right V1 balance for a product going to pilot.
The Outcome
The foundation is solid. The features shipping next—visitor management, audit log UI, owner-managed flats—will all sit cleanly on top of the architecture described here. That's the test of a good architecture: not whether it made today easy, but whether it makes next month easier.
The core system is already running—member management, unit assignment, announcements, complaints, and notifications are live in the current build. You can see the live platform at vaastio.com.
If you're building a multi-tenant SaaS with complex permissions, or a consumer app, I'm always happy to discuss what worked, what didn't, and the lessons learned along the way.