EstateOS is a multi-tenant real estate SaaS foundation built for three surfaces:
- Public marketing and listings
- Buyer transaction portal
- Internal admin operations
The current routing model now separates four distinct surfaces:
/platformEstateOS SaaS marketing site for the platform itself/superadminEstateOS platform-owner dashboard forSUPER_ADMIN/admintenant/company operations for a single real estate company- tenant public marketing and property routes such as
/propertiespublic company-facing discovery experience scoped to one tenant
EstateOS now supports tenant-managed marketer profiles using the company-owned TeamMember domain.
- tenant admins manage marketer/staff profiles under
/admin/team - profiles are tenant-scoped and only visible to buyers/public pages when both
isActiveandisPublishedare true - marketer profile fields now support: full name title photo URL bio profile highlights WhatsApp number email optional resume document link portfolio text and links specialties
- buyers can optionally select a marketer during reservation flow
- selected marketer is persisted on reservation, transaction, and payment records where applicable
- tenant admins can see marketer attribution in payment and transaction views
The codebase is designed for one-company MVP usage today and SaaS-style tenant isolation from day one.
- Next.js App Router
- TypeScript
- Tailwind CSS
- PostgreSQL
- Prisma ORM
- Clerk
- Paystack
- Cloudflare R2
- Inngest
- Upstash Redis
- Resend
- Sentry
Companyis the tenant root in prisma/schema.prisma.- Tenant-owned records carry
companyIdand are queried through server-side tenant helpers. - Public marketing, buyer portal, and admin dashboard are split into route groups under src/app.
- Business reads and writes are concentrated in module services under src/modules.
- Server runtime configuration is validated centrally in src/lib/config.ts, src/lib/env.ts, and src/lib/public-env.ts.
SUPER_ADMINcan operate across tenants.- Non-super-admin users are restricted to one resolved tenant.
SUPER_ADMINplatform routes live under/superadminand are intentionally separate from tenant admin routes under/admin.- Tenant resolution currently supports:
session-based app usage
public fallback via
DEFAULT_COMPANY_SLUGfuture subdomain/custom-domain routing - Tenant-owned reads should use:
requireTenantContextrequirePublicTenantContextfindManyForTenantfindFirstForTenantcountForTenantaggregateForTenant - Privileged writes reject caller-supplied
companyId. - Private document access requires tenant match plus ownership or staff entitlement.
- Payment references and storage keys are tenant-namespaced before leaving the app.
EstateOS now uses a hybrid SaaS monetization model:
- company subscription plans with explicit monthly and annual intervals
- superadmin manual grants and overrides
- transaction-level platform commission on successful property payments
- provider-aware split settlement design so tenant proceeds and EstateOS commission can be separated cleanly
Implemented billing domain records now include:
PlanCompanySubscriptionCompanyBillingSettingsCompanyPaymentProviderAccountCommissionRuleCommissionRecordSplitSettlementBillingEvent
Business rules enforced in code:
- every company can have a current plan
- plans can be monthly, annual, or manually granted
- superadmin grants do not exempt the company from transaction commission
- transaction commission is created from webhook-authoritative successful payments
- transaction access flows are gated by active company plan status
- public marketing and listing reads remain publicly accessible
- Monthly and annual plans are separate plan records with explicit
interval - active access is determined from
status,isCurrent,startsAt,endsAt, andcancelledAt - annual plans are modeled directly, not inferred from monthly price multipliers
- subscription checkout architecture is provider-ready, but live recurring billing is not yet wired in this workspace
- only
SUPER_ADMINcan create plans, assign plans, grant plans, and revoke current subscriptions - grant actions require a reason
- grant and revoke actions are written to both billing events and audit logs
- granted tenants still generate platform commission records on successful transaction payments
- current implementation supports flat per-transaction commission rules
- percentage rules are modeled in the schema and commission logic
- every successful webhook-reconciled payment can upsert:
CommissionRecordSplitSettlement- receipt state
- audit event
- reporting foundations now support:
- active subscriptions
- granted plans
- expired subscriptions
- platform commission earned
- subscription revenue visibility
- payout readiness issues
- settlement calculation is centralized in the billing module
- company payout readiness is derived from
CompanyPaymentProviderAccount - provider-specific split payloads are isolated from unrelated app logic
- current live checkout path remains Paystack-first for property transaction payments
- architecture already supports future Stripe / Flutterwave style settlement strategies through provider-specific metadata builders
- billing and payment domain records are currency-aware
- transaction provider and subscription provider are stored separately in
CompanyBillingSettings - provider account configuration is modeled independently from payment records
- local and international providers can coexist without hardcoding one provider across the whole billing stack
- only Paystack property-payment initialization is live in this workspace today; Stripe/Flutterwave readiness is structural, not falsely claimed as live
Environment parsing is centralized and typed.
- Server env: src/lib/env.ts
- Public client env: src/lib/public-env.ts
- Shared schemas and feature-flag derivation: src/lib/config.ts
The app now validates aggressively when:
- a service group is only partially configured
- public config is malformed
Production-critical services are reported through startup logs and /api/readyz. This keeps next build reproducible in CI and local environments while still making misconfigured production runtime fail operational checks immediately.
DATABASE_URLNEXT_PUBLIC_APP_URLAPP_BASE_URLDEFAULT_COMPANY_SLUGNEXT_PUBLIC_CLERK_PUBLISHABLE_KEYCLERK_SECRET_KEYCLERK_WEBHOOK_SECRETPAYSTACK_SECRET_KEYPAYSTACK_PUBLIC_KEYPAYSTACK_WEBHOOK_SECRETR2_ACCOUNT_IDR2_ACCESS_KEY_IDR2_SECRET_ACCESS_KEYR2_BUCKET_NAME
R2_PUBLIC_BASE_URLMAPBOX_ACCESS_TOKENNEXT_PUBLIC_MAPBOX_ACCESS_TOKENINNGEST_EVENT_KEYINNGEST_SIGNING_KEYINNGEST_BASE_URLUPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKENRESEND_API_KEYEMAIL_FROMSENTRY_DSN
Partial configuration is treated as invalid for grouped services. For example, setting only PAYSTACK_SECRET_KEY without the webhook secret now fails config parsing.
- Install dependencies.
- Copy
.env.exampleto.env.local. - Start PostgreSQL.
- Set at least:
DATABASE_URLNEXT_PUBLIC_APP_URLAPP_BASE_URLDEFAULT_COMPANY_SLUG - Run Prisma validate and generate.
- Run migrations.
- Seed demo data.
- Start the dev server.
- Open
/api/healthand/api/readyz.
npm installcopy .env.example .env.localMinimum local env for DB-backed development:
NODE_ENV="development"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
APP_BASE_URL="http://localhost:3000"
DEFAULT_COMPANY_SLUG="acme-realty"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/realestate_platform?schema=public"Prisma CLI commands load .env.local first and then .env through prisma.config.ts, so the same local database config can be reused across Next.js and Prisma workflows.
If Clerk is not configured in non-production, local demo mode remains available for /portal and /admin.
Option A: local PostgreSQL
CREATE DATABASE realestate_platform;Option B: Docker
docker run --name realestate-postgres ^
-e POSTGRES_USER=postgres ^
-e POSTGRES_PASSWORD=postgres ^
-e POSTGRES_DB=realestate_platform ^
-p 5432:5432 ^
-d postgres:16npm run db:validate
npm run db:generate
npm run db:migrate
npm run db:seednpm run devnpm run devnpm run buildnpm run startnpm run testnpm run typechecknpm run lintnpm run checknpm run db:validatenpm run db:generatenpm run db:migratenpm run db:migrate:deploynpm run db:seed
Use:
npm run db:validate
npm run db:generate
npm run db:migrate
npm run db:seedUse:
npm run db:validate
npm run db:generate
npm run db:migrate:deployNotes:
prisma migrate devis for local development only.prisma migrate deployis the production-safe path.- Seed data is deterministic and intended for development/demo environments.
- The schema and baseline migration should stay aligned; run
npx prisma validateandnpx prisma generateafter schema changes.
- Clerk is required in production.
- In non-production, EstateOS exposes explicit demo access for
/portal,/admin, and/superadmineven if Clerk is configured but no user is signed in. - Development mode now shows a small role switcher so you can jump between public, buyer, tenant admin, and superadmin surfaces without weakening production auth rules.
- Public tenant rendering currently resolves through:
DEFAULT_COMPANY_SLUGfuture host/subdomain lookup authenticated session context when applicable - Clerk webhook sync now validates referenced
companyIdandbranchIdagainst the database before persisting them. - Middleware protects
/portal,/admin, and/superadminonly in production when Clerk is configured.
SUPER_ADMINsees cross-company subscription, billing, payout-readiness, payment, and audit visibility through/superadmin- tenant
ADMINsees only company-scoped operational data through/admin - buyer users continue to operate through
/portal - the default platform entry is
/, which routes into the EstateOS SaaS marketing experience /platformremains a stable alias for the EstateOS SaaS marketing site- tenant public property experiences remain separate and continue to resolve through the active/public tenant context
Paystack is intentionally split into two paths:
POST /api/payments/initializeinitializes provider payment and may persist a pending local payment rowPOST /api/payments/verifyis a read/check helper only and does not mutate authoritative finance statePOST /api/webhooks/paystackis the source of truth for reconciliation
Webhook reconciliation currently handles:
- tenant resolution from namespaced reference
- idempotency guard via provider event identity
- payment upsert/update
- transaction and installment linkage
- receipt upsert
- receipt document persistence
- commission record upsert
- split settlement upsert
- transaction balance update
- transaction stage/milestone update
- audit log write
Buyer payment transparency now renders from persisted database state:
- total payable amount
- amount paid so far
- outstanding balance
- installment schedule
- selected payment plan
- selected marketer
- receipt access
This is current-state rendering, not websocket-based realtime.
Tenant admins can now configure purchase payment plans on properties with:
ONE_TIMEFIXEDCUSTOM
Each plan can define:
- property- or unit-level scope
- title
- duration
- installment count
- deposit percent
- down payment amount
- schedule description
- active state
- installment rows with amount and due offsets
This is purchase-plan modeling only. It does not claim live recurring provider billing for buyer installments.
Transaction payment initialization now also:
- checks active company plan access for transaction flows
- resolves the tenant commission rule
- verifies payout/split readiness for the configured transaction provider
- attaches provider-specific split metadata only through the billing settlement service
- Public brochure route:
/brochures/[slug] - Only documents with
documentType = BROCHUREandvisibility = PUBLICare eligible - Public brochure delivery is separate from private document vault access
- Route-handler redirects now always resolve through
new URL(target, request.url)semantics so internal brochure fallbacks do not throw malformed URL errors - If no public brochure asset URL can be resolved, the route falls back safely to the internal
/brochurepage without exposing private document paths
- Buyer/admin private download route:
/api/documents/[documentId]/download - Buyer/admin private receipt render route:
/api/receipts/[receiptId]/download - Access requires tenant match and ownership/staff entitlement
- Upload signing uses tenant-namespaced keys
- Missing R2 config falls back safely in non-production flows instead of exposing internals
- receipts are rendered with tenant company branding and company contact data
- buyer access remains ownership-safe
- tenant admins can also view tenant receipts
- current implementation is a private render-first receipt download pipeline
- it does not pretend background PDF generation or email delivery is live unless separately configured
/api/healthbasic liveness plus non-secret dependency summary and runtime readiness summary/api/readyzreadiness endpoint with DB connectivity status, production-readiness checks, and safe dependency summary
These endpoints do not expose secrets.
- Sentry initialization is wired through instrumentation.ts and src/lib/sentry.ts
- Root UI errors are captured in src/app/error.tsx
- Critical webhook failures now emit safe logs and error capture
- Startup readiness is logged once through src/lib/ops/startup.ts
The repo is prepared for Next.js production deployment and includes vercel.json for a straightforward Vercel setup.
- Provision PostgreSQL.
- Set all required production env vars.
- Configure Clerk frontend and backend keys plus webhook URL.
- Configure Paystack callback and webhook URLs.
- Configure R2 bucket and credentials.
- Run:
npm run db:validatenpm run db:generatenpm run db:migrate:deploy - Deploy the app.
- Verify:
/api/health/api/readyzClerk auth flow Paystack webhook flow brochure download flow private document flow - Confirm
/api/readyzreportsok: truebefore exposing the environment to internal users.
- PostgreSQL
- Clerk
- Paystack
- Cloudflare R2
- billing plans seeded or created by superadmin
- at least one active
CommissionRule CompanyBillingSettingsfor each live tenant- active payout configuration in
CompanyPaymentProviderAccount - Paystack webhook delivery for authoritative transaction commission creation
- Mapbox
- Resend
- Upstash Redis
- Inngest
- Sentry
Verified here:
npm run testnpm run typechecknpm run lintnpm run buildnpx prisma validatenpx prisma generate
Billing-specific runtime confidence added here:
- active plan calculation
- granted-plan behavior
- commission-on-granted-plan behavior
- split-settlement preview generation
- billing plan and manual grant validation rules
Build verification in this workspace now runs without requiring live production secrets at import time. Production readiness is surfaced through runtime checks instead of blocking next build.
- Real Postgres migrate/seed execution against a running database
- Real Clerk auth and webhook round-trips
- Real Paystack initialize/verify/webhook round-trips
- Real R2 object upload/download behavior
- Real Resend delivery
- Real Upstash, Inngest, and Sentry behavior in staging/production
- Real subscription checkout / renewal provider flow for monthly and annual SaaS billing
- Real provider payout account provisioning for Paystack split settlement and future international providers
EstateOS is now development-ready in the following sense:
- env parsing is typed and centralized
- local bootstrap is explicit and reproducible
- Prisma workflow has local and production-safe script paths
- health and readiness endpoints exist
- client/server config boundaries are cleaner
- production-only missing-core-service states are surfaced through runtime readiness checks and startup logs
EstateOS now has a dedicated SaaS marketing surface under /platform with:
- Home
- Features
- How it works
- Pricing
- Why EstateOS
- FAQ
- Contact / demo request
This site is intentionally separate from tenant public property routes so the SaaS product story does not get mixed with a tenant company's listing website.
Implemented in the current pass:
- brochure redirect bug fix for route-handler-safe absolute redirects
- dedicated
SUPER_ADMINplatform dashboard and company oversight views - public EstateOS SaaS marketing site under
/platform
Still follow-up work:
- deeper superadmin action surfaces beyond inspection and navigation
- tenant-aware custom-domain routing for platform-vs-tenant host separation
- richer platform marketing lead capture and CRM handoff for the EstateOS SaaS site
- richer marketer profile media and resume upload UX
- provider-side recurring buyer installment collection is still follow-up work
- Live external-service behavior is still unproven until real staging credentials are used
- Demo fallbacks remain intentionally available in non-production, so teams need discipline not to confuse demo behavior with live integrations
- Sentry wiring is basic and should be expanded with richer tracing and release configuration before high-volume production use
- Receipt documents are persisted, but receipt PDF generation is still deferred
- Subscription checkout, renewal charging, and dunning are provider-ready in schema/service design but not live yet