EstateOS
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:
/platform
EstateOS SaaS marketing site for the platform itself
/superadmin
EstateOS platform-owner dashboard for SUPER_ADMIN
/admin
tenant/company operations for a single real estate company
- tenant public marketing and property routes such as
/properties
public company-facing discovery experience scoped to one tenant
Product Focus: Developer Sales And Collections OS
EstateOS is now positioned around one primary workflow:
- Lead
- Inspection
- Reservation
- Payment
- Receipt
- Collections
The product center of gravity is now the tenant admin Deal Board at /admin.
/admin is the canonical operator surface for tracking buyers, reserved deals, payment-pending deals, paid deals, and overdue collections
- the board is intentionally revenue-first:
buyer
property or unit
total deal value
amount paid
outstanding balance
owner / marketer
due timing
next action
- onboarding inside the Deal Board is intentionally lightweight:
confirm company
add first property
add team member
create first deal
- tenant admins can also load a realistic sample workspace from the Deal Board to make demos and pilots feel like a live developer sales team, without relying on hidden fallback behavior
Internal Funnel Instrumentation
EstateOS now writes lightweight tenant-aware product events into ActivityEvent for the core workflow:
company.onboarded
property.created
property.first_created
team_member.added
team_member.first_added
deal.created
deal.first_created
inquiry.created
inspection.booked
reservation.created
payment_request.sent
payment.completed
payment.overdue_detected
deal.closed
sample_workspace.loaded
These events support:
- the Deal Board activity feed
- focused admin analytics
- clearer pilot/demo visibility into activation and collections behavior
Tenant Staff Directory And Buyer Guidance
EstateOS now supports tenant-managed marketer profiles using the company-owned TeamMember domain.
- tenant admins manage staff and marketer profiles under
/admin/team
- profiles are tenant-scoped and only visible to buyers/public pages when both
isActive and isPublished are true
- staff profile fields now support:
full name
title
photo URL
bio
staff code
office location
profile highlights
WhatsApp number
email
optional resume document link
portfolio text and links
social links
specialties
- public tenant directory now lives under
/team
- optional public profile detail pages now live under
/team/[slug]
- public contact actions render only when valid data exists:
mailto: for email
https://wa.me/... for WhatsApp
- tenant admins can generate branded staff ID cards from
/admin/team
- ID cards are render-first HTML downloads and include:
company logo
company name and address
staff name and role
profile photo
contact details
staff code
QR code to the tenant public site
- QR destination rules are:
custom domain when configured
otherwise tenant public route fallback under
/properties
- 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.
Temporary Production Database Safety Guard
Local development must use a separate Supabase project. Until that split is complete, configure:
PRODUCTION_DATABASE_PROJECT_REF=epxbejutuodmnsdfvcjr
PRODUCTION_DATABASE_HOST=aws-0-eu-west-1.pooler.supabase.com
ALLOW_PRODUCTION_DB_WRITES=false
When local development points at the production database, EstateOS blocks destructive seeds and demo, mock, or sample-data writes. npm run db:seed is permanently blocked against the known production database. Use npm run audit:demo-data for a read-only inventory.
R2_PUBLIC_BASE_URL is recommended for public branding and property asset delivery. Private uploads, downloads, and contract documents use signed R2 URLs and do not require the public base URL.
Wishlist And Client Activity Intelligence
EstateOS now deepens buyer intent tracking through a tenant-scoped wishlist workflow built on the existing SavedProperty domain.
- buyers can save properties to wishlist from public property pages after sign-in
- each wishlist record is tenant-scoped and property-linked
- re-saving an active wishlist item removes it; saving again reactivates the same record instead of creating duplicates
- tenant admins receive in-app notifications when a client adds a property to wishlist
- wishlist records now support:
expiry windows
reminder sent timestamps
selected marketer linkage
follow-up status
follow-up notes
optional internal follow-up owner
- property admins can configure:
wishlistDurationDays
wishlistReminderEnabled
- buyer wishlist state is shown in
/portal/saved with:
saved date
expiry state
time-left label
selected marketer context
- tenant admins can inspect each client in
/admin/clients/[clientId] and see:
wishlist items
inquiries
inspections
reservations
payments
documents
KYC state
recent activity
- tenant follow-up support currently includes:
WhatsApp deep-link generation
follow-up status tracking
follow-up note capture
internal follow-up owner assignment
Wishlist Duration And Reminder Rules
- wishlist expiry is currently configured at the property level
- if a property does not define a custom wishlist duration, EstateOS falls back to a 14-day default
- reminder eligibility is currently "expiring within 3 days, still active, reminder not yet sent"
- reminder workflow is implemented as:
service functions in
src/modules/wishlist/service.ts
an admin-callable sweep route at /api/admin/wishlists/reminders/run
an Inngest event hook for wishlist/reminder.send
- live scheduled automation still depends on production Inngest/cron setup
- email reminders use Resend when configured and fail safely to non-delivery behavior when email is disabled
Marketer Ranking Logic
- Top Marketers now appears on both the tenant public
/team page and the tenant public /properties discovery page
- public Top Marketers supports
WEEKLY and MONTHLY ranking views only
- ranking is tenant-scoped and fetched from current persisted DB activity, not demo data or vanity metrics
StaffProfile.teamMemberId is now the canonical relation for inquiry and inspection attribution
- existing staff-to-marketer links are backfilled during migration using the previous email/staff-code heuristic, then live ranking reads the explicit relation
- attribution precedence is:
- buyer-selected marketer persisted on reservation, transaction, and payment records
- fallback staff assignment on qualified inquiries and handled inspections when no marketer was explicitly selected
- revenue attribution for admin analytics is stricter and uses:
payment.marketerId
transaction.marketerId
reservation.marketerId
- no attribution if all three are empty
- current weighted score uses:
completed deals
successful payments
linked reservations
handled inspections
qualified or converted inquiries
wishlist saves linked to the marketer
- only active, published tenant team members are eligible for public ranking
- public marketer profile pages now also show the current performance snapshot and star rating
- public marketer surfaces never expose private revenue figures
- star ratings are derived from the composite score with bounded output from 3.0 to 5.0 so low-signal profiles are not falsely over-promoted
- tenant admins now have a dedicated
/admin/marketers view for the full ranked list, private revenue analytics, score breakdowns, and recent trend indicators
- admin marketer analytics support
WEEKLY, MONTHLY, and LIFETIME period views
- daily
MarketerRankingSnapshot rows are written by the operational automation sweep so historical rank and score movement can be compared safely over time
- buyer payment surfaces now resolve marketer attribution in this order:
payment.marketerId
transaction.marketerId
reservation.marketerId
- marketers do not have a private self-dashboard yet; admin-only visibility is intentional because marketers do not have login in the current phase
- snapshot scheduling depends on the same automation runner used for other operational jobs; deployment still needs live cron or Inngest scheduling to execute it automatically
Property Verification And Anti-Ghosting
EstateOS now includes a trust-layer for property freshness so stale or ghost inventory does not continue appearing as valid public stock.
- every property now carries:
lastVerifiedAt
verificationStatus
verificationDueAt
isPubliclyVisible
autoHiddenAt
verificationNotes
- verification state is centralized in
src/modules/properties/verification.ts
- default thresholds are:
verified within 7 days
stale from day 8 through day 30
hidden after day 30
UNVERIFIED listings are hidden from public routes until a tenant admin verifies them
HIDDEN listings never appear in public listing or detail queries
STALE listings may remain public, but they render with visible warning labels instead of looking fresh
- tenant admins can manually verify from
/admin/listings
- admin verification writes audit logs and resets the next due window
- admin operators can also run a verification sweep from
/api/admin/properties/verification/run
- Inngest support is wired through the
property/verification.sync event for production scheduling
Public Trust Behavior
- public property listing and detail queries now filter through verification-safe services
- hidden or never-verified inventory is excluded before it reaches public UI
- visible properties now render trust badges such as:
Verified 2 days ago
Last updated 14 days ago
- brochure access for hidden or unverified properties remains blocked because brochure lookup follows the same public property visibility rules
Admin Responsibilities
- tenant admins are responsible for re-verifying public inventory on time
- the admin dashboard now shows:
needs verification
stale listings
hidden listings
recently verified listings
- notifications are sent for:
listings nearing the stale threshold
listings that were auto-hidden
Follow-up Improvements
- verification thresholds are hardcoded today for safety and consistency
- tenant-configurable verification policies can be added later without changing the public query boundary
- scheduled production automation still depends on live Inngest or cron wiring in deployment
Tenant Branding Customization
EstateOS now supports a controlled tenant-branding workflow for company-owned experiences without turning the product into a freeform website builder.
- tenant branding is stored in
SiteSettings as:
draftBrandingConfig
publishedBrandingConfig
brandingPublishedAt
- tenant admins manage branding from
/admin/settings/branding
- branding supports:
primary color
secondary color
accent color
background style
background color
surface color
text mode
logo URL
favicon URL
optional hero image URL
button style preset
card style preset
navigation style preset
Draft vs Published Workflow
- edits save into draft branding only
- preview panels in the branding studio render the draft state
- live tenant experiences render published branding only
- tenant admins can:
save draft
reset draft to published
publish branding
- publish actions are written to audit logs
Theme Application Boundaries
- tenant public routes use branding more expressively
- tenant admin and buyer portal apply branding in a restrained way through shared CSS variables
- platform marketing routes under
/platform and platform-owner routes under /superadmin do not inherit tenant branding
- there is no arbitrary CSS injection or freeform style editing
Safe Design Rails
- colors must be valid 6-digit hex values
- publish is blocked if key app-surface contrast becomes unsafe
IMAGE_HERO requires a hero image URL before publish
- navigation, button, and card styling are constrained to supported presets
- tenant branding assets are URL-based in this pass and only affect tenant-owned public/admin/portal surfaces
Branding Presets And Preview
- the branding studio now includes curated presets such as:
Modern Blue
Luxury Gold
Corporate Clean
Elegant Dark
Warm Terracotta
Emerald Professional
- presets apply to the draft branding only until the tenant admin publishes
- admins can preview branding in both:
Desktop mode
Mobile mode
- preview remains representative and clearly marked as
DRAFT PREVIEW
Universal Upload System
EstateOS now uses one tenant-scoped upload pattern for reusable media and document handling.
- signed upload preparation lives at
/api/uploads/sign
- document finalization lives at
/api/uploads/documents
- public asset resolution lives at
/api/assets/public/[...key]
- upload purposes are centrally mapped in
src/modules/uploads/config.ts
Where Shared Uploads Now Apply
- tenant branding assets:
logo
favicon
hero image
- tenant media library:
reusable asset picking for branding, staff, property, brochure, and document-backed uploads
- staff management:
profile image
resume upload
- property management:
brochure upload
property media upload
- buyer KYC:
private KYC document upload
Public vs Private Asset Rules
- public assets use controlled public storage domains such as:
branding
property-media
staff-media
- private documents remain document-vault backed and are not exposed as public assets:
KYC
resumes
- brochures remain public documents, but are still modeled separately from private vault documents
- uploads remain tenant-scoped through namespaced storage keys and tenant-authenticated routes
Future Enhancements
- stronger media-library indexing and bulk management
- tenant-side upload analytics and optimization metadata
Media Library And Responsive Polish
EstateOS now includes a tenant-scoped media library and final UX polish for image-heavy surfaces.
/admin/assets provides a reusable media library for tenant-owned assets
- tenant admins can:
search assets
filter by purpose
filter by image vs document
filter by public vs private visibility
choose an existing asset from the branding/staff/property flows
- media library respects tenant scoping and does not expose cross-tenant assets
- public/private distinctions stay intact:
public branding/property/staff media can be previewed
private documents remain document-backed and are not exposed through public asset URLs
Image Rendering And Optimization Notes
- EstateOS now uses a shared image wrapper for key public and tenant image surfaces
- image handling is improved through:
consistent sizing presets
responsive
sizes hints
safer fallback to unoptimized rendering for unknown external hosts
- this is a real rendering/layout optimization improvement
- it does not claim remote provider-side image transforms where those are not available
Logo-Based Auto Theme
- branding studio now supports an optional
Generate from logo action
- the selected logo is sampled client-side to produce a safe draft palette suggestion
- generated theme applies to draft only after explicit admin confirmation
- if extraction fails or the logo cannot be sampled safely, the draft is not mutated
Drag-And-Drop Multi Upload
- property media management now supports drag-and-drop multi-upload
- admins can upload multiple gallery assets in one pass, preview them, and reorder them
- uploads still use the shared tenant-scoped upload pipeline and existing visibility rules
Responsive Hardening Notes
- tenant admin and buyer dashboard shells are now more mobile-friendly
- branding preview supports explicit desktop/mobile framing
- media picker dialogs, asset grids, and team/property image surfaces were tightened for cleaner mobile behavior
- the goal is responsive usability, not a separate mobile-only code path
Stack
- Next.js App Router
- TypeScript
- Tailwind CSS
- PostgreSQL
- Prisma ORM
- Clerk
- Paystack
- Cloudflare R2
- Inngest
- Upstash Redis
- Resend
- Sentry
Architecture Summary
Company is the tenant root in prisma/schema.prisma.
- Tenant-owned records carry
companyId and 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.
Tenant Model Summary
SUPER_ADMIN can operate across tenants.
- Non-super-admin users are restricted to one resolved tenant.
SUPER_ADMIN platform routes live under /superadmin and are intentionally separate from tenant admin routes under /admin.
- Production superadmin access is fail-closed. Configure the private
SUPERADMIN_EMAILS environment variable with a comma-separated platform-owner allowlist, then run npm run audit:superadmins to report any existing SUPER_ADMIN assignment outside that allowlist.
- Secure platform-owner setup:
Set
SUPERADMIN_EMAILS in the production environment.
Sign in once normally with the allowlisted email so Clerk synchronization creates the EstateOS user.
Run npm run grant:superadmin -- --email [email protected] from a trusted operator environment with production database access.
Run npm run audit:superadmins, then access /superadmin.
- Tenant resolution currently supports:
session-based app usage
central platform and portal hosts
tenant custom domains
tenant subdomains
controlled public fallback via
DEFAULT_COMPANY_SLUG only on known central/dev hosts
- Tenant-owned reads should use:
requireTenantContext
requirePublicTenantContext
findManyForTenant
findFirstForTenant
countForTenant
aggregateForTenant
- 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.
- Public staff directory reads return only active + published
TeamMember rows for the resolved tenant.
- Staff ID-card generation is admin-only and uses the requesting tenant context before loading branding or profile data.
Custom Domains And Central Auth
- Current production-ready phase:
- tenant public browsing can live on tenant custom domains
- authenticated flows are centralized on
PORTAL_BASE_URL
- tenant public CTAs build central sign-in redirects with tenant context and intended destination
- Tenant context is preserved across central-domain auth through:
- safe
tenant and host redirect params
- post-auth handoff at
/auth/complete
- a server-only tenant hint cookie used only when host resolution is absent
- Authenticated tenant app routes never let a tenant hint override the signed-in user's tenant.
- Unknown production custom domains do not silently fall back to
DEFAULT_COMPANY_SLUG.
- Future Clerk satellite-domain rollout is prepared by centralizing:
resolveCentralAuthUrl
resolveTenantPublicUrl
buildAuthRedirect
buildReturnUrl
- EstateOS does not claim satellite-domain session sharing is already live without real Clerk production setup.
Billing And Monetization Model
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:
Plan
CompanySubscription
CompanyBillingSettings
CompanyPaymentProviderAccount
CommissionRule
CommissionRecord
SplitSettlement
BillingEvent
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 vs Annual Rules
- Monthly and annual plans are separate plan records with explicit
interval
- active access is determined from
status, isCurrent, startsAt, endsAt, and cancelledAt
- 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
Superadmin Grants
- only
SUPER_ADMIN can 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
Commission Model
- 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:
CommissionRecord
SplitSettlement
- receipt state
- audit event
- reporting foundations now support:
- active subscriptions
- granted plans
- expired subscriptions
- platform commission earned
- subscription revenue visibility
- payout readiness issues
Split Payment Model
- 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
International Provider Readiness
- 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 Model
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.
Required In Production
DATABASE_URL
DIRECT_URL for Prisma migrations and deploy workflows
NEXT_PUBLIC_APP_URL
APP_URL (https://estateos.tech in production)
APP_BASE_URL
DEFAULT_COMPANY_SLUG
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
CLERK_SECRET_KEY
SUPERADMIN_EMAILS with the private comma-separated platform-owner allowlist
PAYSTACK_SECRET_KEY
PAYSTACK_PUBLIC_KEY
PAYSTACK_WEBHOOK_SECRET
R2_ACCOUNT_ID
R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
R2_BUCKET_NAME
Optional But Supported
CLERK_WEBHOOK_SECRET for verified /api/webhooks/clerk ingestion
R2_PUBLIC_BASE_URL
MAPBOX_ACCESS_TOKEN
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN
INNGEST_EVENT_KEY
INNGEST_SIGNING_KEY
INNGEST_BASE_URL
UPSTASH_REDIS_REST_URL
UPSTASH_REDIS_REST_TOKEN
REALTIME_TRANSPORT=polling
Realtime defaults to polling in production. The current SSE implementation uses a process-local event emitter only; Redis pub/sub is not wired yet. Set REALTIME_TRANSPORT=sse only for controlled environments where process-local delivery is acceptable.
RESEND_API_KEY
EMAIL_FROM
SENTRY_DSN
Service Group Rule
Partial configuration is treated as invalid for grouped services. For example, setting only PAYSTACK_SECRET_KEY without the webhook secret now fails config parsing.
Production data hygiene and manual tenant access
Audit historical demo records and role integrity without mutating data:
npm run audit:data-hygiene
npm run audit:tenant-readiness -- --companySlug blueprint-urban-residences
Review the report before running the conservative cleanup dry run. Cleanup never deletes companies, properties, payments, transactions, generated legal documents, KYC documents, or Blueprint tenant data.
npm run cleanup:data-hygiene -- --dry-run
npm run cleanup:data-hygiene -- --apply --confirm "CLEAN_DEMO_DATA"
Provision an existing tenant admin without storing a password in EstateOS:
npm run provision:tenant-admin -- --companySlug blueprint-urban-residences --email [email protected] --name "Admin Name"
After provisioning, invite the email from Clerk or send a Clerk password setup/reset link. EstateOS creates a manual:<email> placeholder only when needed and links the Clerk identity by email on first sign-in. For an unavoidable temporary password, create it only in Clerk, require a first-login change where supported, and never print it in EstateOS logs.
Local Developer Bootstrap Checklist
- Install dependencies.
- Copy
.env.example to .env.local.
- Create or choose a Supabase project.
- Set at least:
DATABASE_URL
DIRECT_URL
NEXT_PUBLIC_APP_URL
APP_BASE_URL
DEFAULT_COMPANY_SLUG
- Run Prisma validate and generate.
- Run migrations.
- Seed demo data.
- Start the dev server.
- Open
/api/health and /api/readyz.
Local Setup
1. Install
npm install
2. Configure Env
copy .env.example .env.local
Minimum local env for DB-backed development:
NODE_ENV="development"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
NEXT_PUBLIC_PLATFORM_BASE_URL="http://localhost:3000"
NEXT_PUBLIC_PORTAL_BASE_URL="http://localhost:3000"
APP_BASE_URL="http://localhost:3000"
PLATFORM_BASE_URL="http://localhost:3000"
PORTAL_BASE_URL="http://localhost:3000"
DEFAULT_COMPANY_SLUG="acme-realty"
DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-SUPABASE-PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true&connection_limit=1&sslmode=require"
DIRECT_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-SUPABASE-PROJECT-REF].supabase.co:5432/postgres?sslmode=require"
Prisma CLI commands load .env.local first and then .env through prisma.config.ts, so the same Supabase database config can be reused across Next.js and Prisma workflows.
Local demo mode is only available when ESTATEOS_ENABLE_DEV_BYPASS=true. It is intentionally disabled by default, even in development.
Local development keeps the central-auth architecture easy to test:
NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_PLATFORM_BASE_URL, and NEXT_PUBLIC_PORTAL_BASE_URL can all stay on localhost.
DEFAULT_COMPANY_SLUG gives signed-in areas (/admin, /portal) a tenant without multi-domain DNS. It does not apply to public pages — see below.
- central auth redirect helpers collapse back to the same localhost origin when portal and platform share the same dev host.
Opening A Tenant Site Locally
npm run dev and then http://localhost:3000 renders the platform marketing
page, not a tenant site. That is correct behavior, not a broken database:
public pages resolve the tenant from the host, and DEFAULT_COMPANY_SLUG
is deliberately not consulted for them, so a fallback can never serve one
tenant's public site on another host in production.
Use either of these instead (the dev server also prints this hint, naming the
companies present in your database, whenever it falls back to the platform page):
# 1. Query parameter — needs DEV_ACCESS_MODE=true in .env.local
http://localhost:3000/?devTenant=acme-realty
# 2. Tenant host — no env flag needed; *.localhost resolves to loopback in
# most browsers, otherwise add "127.0.0.1 acme-realty.localhost" to your
# hosts file
http://acme-realty.localhost:3000
Option 1 is the intended day-to-day workflow. Option 2 is the closer match to
production, because it exercises the same host-based resolution that a real
tenant domain uses, and it is what the E2E suite drives.
If the hint reports that no companies exist, the database simply has not been
seeded:
createdb estateos_dev # or: psql -c "CREATE DATABASE estateos_dev"
npm run db:migrate:deploy # applies prisma/migrations to that database
npm run db:seed # creates the acme-realty demo tenant
Database Setup (Supabase)
1. Create A Supabase Project
- Create a new Supabase project in the Supabase dashboard.
- Open
Project Settings -> Database.
- Copy both connection strings:
- pooled connection via the Supabase pooler on port
6543
- direct Postgres connection on port
5432
2. Add Supabase URLs To .env.local
Use:
DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-SUPABASE-PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true&connection_limit=1&sslmode=require"
DIRECT_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-SUPABASE-PROJECT-REF].supabase.co:5432/postgres?sslmode=require"
Meaning:
DATABASE_URL is the pooled runtime connection that the Next.js app uses through Prisma.
DIRECT_URL is the direct connection Prisma uses for migrate dev and other schema operations.
Both URLs should include sslmode=require for Supabase Postgres.
3. Prisma With Supabase
Use:
npm run db:validate
npm run db:generate
npm run db:migrate
npm run db:seed
For staging or production deployments, use:
npm run db:validate
npm run db:generate
npm run db:migrate:deploy
4. Run The App
npm run dev
Scripts
npm run dev
npm run build
npm run start
npm run test
npm run typecheck
npm run lint
npm run check
npm run db:validate
npm run db:generate
npm run db:migrate
npm run db:migrate:deploy
npm run db:seed
Database And Migration Workflow
Local Development
Use:
npm run db:validate
npm run db:generate
npm run db:migrate
npm run db:seed
Production / Staging
Use:
npm run db:validate
npm run db:generate
npm run db:migrate:deploy
Notes:
prisma migrate dev is for local development only.
prisma migrate deploy is the production-safe path.
- Vercel builds intentionally run only
prisma generate and next build. Run npm run db:migrate:deploy separately in a controlled CI or release step before deploying application code.
- Any commit that changes
prisma/schema.prisma or adds a prisma/migrations/* folder must have migrations deployed before the new code path is used in production. Do not patch application code to hide missing-column errors; deploy the migration first.
DATABASE_URL should point to the Supabase pooler for runtime traffic.
DIRECT_URL should point to the direct Supabase Postgres host for migrations.
- Seed data is deterministic and intended for development/demo environments.
- The schema and baseline migration should stay aligned; run
npx prisma validate and npx prisma generate after schema changes.
If production reports Prisma missing-column drift, verify migration state in Supabase before debugging feature code:
SELECT migration_name, finished_at, rolled_back_at
FROM "_prisma_migrations"
WHERE migration_name IN (
'0036_team_invitation_branch',
'0037_property_location_mapbox',
'0038_property_location_boundary',
'0039_team_invitation_invited_by_user_id'
)
ORDER BY migration_name;
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'TeamMemberInvitation'
ORDER BY ordinal_position;
Auth, Session, And Tenancy Runtime Rules
- Clerk is required in production.
- In non-production, EstateOS exposes explicit demo access for
/portal, /admin, and /superadmin only when ESTATEOS_ENABLE_DEV_BYPASS=true.
- Development mode shows the small role switcher only when dev bypass is explicitly enabled, so local convenience does not leak into production behavior.
- Public tenant rendering currently resolves through:
DEFAULT_COMPANY_SLUG
future host/subdomain lookup
authenticated session context when applicable
- Clerk webhook sync now validates referenced
companyId and branchId against the database before persisting them.
- Middleware protects
/portal, /admin, and /superadmin only in production when Clerk is configured.
Platform Owner Vs Tenant Admin
SUPER_ADMIN
sees cross-company subscription, billing, payout-readiness, payment, and audit visibility through /superadmin
- tenant
ADMIN
sees 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
/platform remains 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
Payment Authority Model
Paystack is intentionally split into two paths:
POST /api/payments/initialize
initializes provider payment and may persist a pending local payment row
POST /api/payments/verify
is a read/check helper only and does not mutate authoritative finance state
POST /api/webhooks/paystack
is 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 Progress
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.
Inquiry Workflow
- Public visitors can submit tenant-scoped property or general inquiries through
/api/inquiries.
- Authenticated buyers can submit the same inquiry flow from inside the portal dashboard.
- Inquiry lifecycle now supports:
NEW
CONTACTED
INSPECTION_BOOKED
QUALIFIED
CONVERTED
CLOSED
LOST
- Tenant admins manage inquiries from
/admin/leads, including status changes, assignable staff routing, and internal notes.
Inspection Workflow
- Public property pages can submit inspection requests through
/api/inspections.
- Inspection bookings are persisted and tenant-scoped.
- Inspection lifecycle now supports:
REQUESTED
CONFIRMED
RESCHEDULED
COMPLETED
CANCELLED
NO_SHOW
- Tenant admins manage bookings from
/admin/bookings.
- Buyers can review persisted booking state from
/portal/inspections.
Notification Behavior
- In-app notifications remain the required channel.
- This pass adds practical coverage for:
inquiry received
inquiry assigned
inspection requested
inspection updated
reservation created
transaction stage updates
payment and receipt visibility
- Email uses the current Resend foundation when configured; otherwise notification records still persist and email falls back to demo behavior.
- Superadmin notifications remain separate from tenant notification streams.
Sales Pipeline Behavior
- Tenant admins now have a dedicated
/admin/pipeline view.
- The pipeline summarizes:
leads / inquiries
inspections
reservations
payments in progress
completed deals
- Cards are drill-down oriented and link back to the operational lists for leads, bookings, transactions, and payments.
Purchase Installment Configuration
Tenant admins can now configure purchase payment plans on properties with:
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
Storage Model
Public Brochures
- Public brochure route:
/brochures/[slug]
- Only documents with
documentType = BROCHURE and visibility = PUBLIC are 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
/brochure page without exposing private document paths
Private Documents
- 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
Branded Receipt Behavior
- 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
Health And Readiness Endpoints
/api/health
basic liveness plus non-secret dependency summary and runtime readiness summary
/api/readyz
readiness endpoint with DB connectivity status, production-readiness checks, and safe dependency summary
These endpoints do not expose secrets.
Observability And Error Handling
- 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
Deployment Readiness
The repo is prepared for Next.js production deployment and includes vercel.json for a straightforward Vercel setup.
Production Checklist
- Provision PostgreSQL.
- Set all required production env vars.
- Configure Clerk frontend and backend keys plus webhook URL.
- Configure platform and portal domains:
NEXT_PUBLIC_PLATFORM_BASE_URL
NEXT_PUBLIC_PORTAL_BASE_URL
PLATFORM_BASE_URL
PORTAL_BASE_URL
- Configure Paystack callback and webhook URLs.
- Configure R2 bucket and credentials.
- Run migrations before enabling schema-dependent features:
npm run db:validate
npm run db:generate
npm run db:migrate:deploy
- Deploy the app only after
npm run db:migrate:deploy succeeds against production. The Vercel build does not run migrations or require DIRECT_URL network access.
- Verify:
/api/health
/api/readyz
Clerk auth flow
central portal redirect flow from a tenant public domain
Paystack webhook flow
brochure download flow
private document flow
- Confirm
/api/readyz reports ok: true before exposing the environment to internal users.
Mandatory Services For First Production Deployment
- PostgreSQL
- Clerk
- Paystack
- Cloudflare R2
Mandatory Services For Hybrid Monetization In Production
- billing plans seeded or created by superadmin
- at least one active
CommissionRule
CompanyBillingSettings for each live tenant
- active payout configuration in
CompanyPaymentProviderAccount
- Paystack webhook delivery for authoritative transaction commission creation
Services That Can Be Disabled In Lower Environments
- Mapbox
- Resend
- Upstash Redis
- Inngest
- Sentry
Runtime Verification In This Workspace
Verified here:
npm run test
npm run typecheck
npm run lint
npm run build
npx prisma validate
npx 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.
What Still Requires Live Credentials Or Infrastructure
- 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
Development-Ready Status
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
Platform Marketing Site
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.
Current Expansion Pass Status
Implemented in the current pass:
- brochure redirect bug fix for route-handler-safe absolute redirects
- dedicated
SUPER_ADMIN platform 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
Known Remaining Risks
- 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
Operating System Layer
EstateOS now includes a tenant-scoped operating-system layer aimed at daily use by real estate companies.
Daily Dashboard
/admin is now a real today dashboard.
- It surfaces follow-up work, expiring wishlists, upcoming inspections, overdue payments, pipeline counts, urgent alerts, and quick actions.
- All dashboard metrics are tenant-scoped and derived from persisted data.
Payment Enforcement Model
- Transactions now carry a deal-level
paymentStatus (PENDING, PARTIAL, COMPLETED, OVERDUE).
- Reservation-created transactions are initialized with payment-state context and next due date when a payment plan exists.
- Admin stage updates cannot move a deal into final-payment or handover completion while money is still outstanding.
- Successful payment reconciliation remains webhook-authoritative and now re-syncs transaction payment state.
- Receipts remain accessible in both buyer and admin flows.
Automation Flows
- Wishlist reminder sweep remains callable and can run through Inngest when configured.
- Operational automation now supports:
- overdue payment reminders
- inspection reminders within 24 hours
- client follow-up attention alerts
- No fake scheduling is assumed. In local/dev environments, these flows are callable through admin routes and Inngest wiring is production-ready but still requires live scheduler configuration.
Tenant CMS / Config UI
/admin/settings lets tenant admins manage:
- company identity and support channels
- logo and brand colors
- default wishlist duration
- future-ready verification settings
- payment display defaults
- staff public visibility preferences
- active-plan guardrails for operations
- These settings are persisted in tenant-scoped
Company, SiteSettings, and CompanyBillingSettings records.
Data Lock-In
- Admins can now export tenant-scoped CSV data for:
- clients
- transactions
- payments
- Client profiles remain the central operational record for KYC, wishlist, inquiries, inspections, reservations, payments, receipts, documents, and timeline activity.
Live-Service Caveats
- Resend is required to fully verify email reminder delivery.
- Inngest or an external scheduler is required for true production automation scheduling.
- Paystack remains the payment authority for live reconciliation; no client-side success state is trusted.
Final Operational Hardening Pass
Payment Placeholder Model
- Every new reservation now creates a tenant-scoped placeholder payment record immediately.
- Placeholder payments use
AWAITING_INITIATION so a deal never exists without payment context.
- This placeholder is not authoritative payment proof. Webhook reconciliation still owns real payment success.
Automation Scheduling Model
- Operational automation is now wrapped in a scheduled-job service that writes
BackgroundJobLog records.
/api/admin/automation/run remains available for manual runs.
/api/internal/automation/run is the cron-safe endpoint for staging/production and requires CRON_SECRET.
- Current scheduled sweep covers:
- wishlist reminders
- overdue payment reminders
- inspection reminders
- follow-up attention alerts
- property verification sync
- payment-request expiry sync
Verification Threshold Settings
- Property verification thresholds are now tenant-configurable in
/admin/settings.
- Supported settings:
verificationFreshDays
verificationStaleDays
verificationHideDays
verificationWarningReminderDays
- Safe validation enforces
fresh < stale < hide.
- Hidden listings still never leak publicly, and public listing queries now rely on persisted verification visibility rather than scattered date math.
Payment Request Workflow
- Tenant admins can now create payment requests from
/admin/payments.
- Payment requests are provider-neutral at the domain level and support:
HOSTED_CHECKOUT
BANK_TRANSFER_TEMP_ACCOUNT
DEDICATED_VIRTUAL_ACCOUNT
MANUAL_BANK_TRANSFER_REFERENCE
CARD_LINK
- Request statuses:
DRAFT
SENT
AWAITING_PAYMENT
PAID
EXPIRED
CANCELLED
- Buyers can view request state, due dates, and payment instructions in
/portal/payments.
Paystack Temporary Transfer Account Notes
- EstateOS now supports a Nigeria-specific Paystack payment-request path for bank-transfer-based collection.
- This is implemented as provider-specific behavior inside the payment-request architecture, not as a global assumption.
- If Paystack returns temporary transfer account details, EstateOS persists and shows:
- bank name
- account number
- account name
- expiry metadata
- If Paystack does not return those details in the current environment, EstateOS falls back to showing the secure hosted payment link and does not fake transfer account data.
Webhook Authority Reminder
- Payment request creation, placeholder payments, and checkout initialization do not prove money was received.
charge.success / webhook reconciliation remains the source of truth for:
- real payment success
- receipt generation
Central Portal/Auth Domain Model
PLATFORM_BASE_URL is the public EstateOS platform root.
PORTAL_BASE_URL is the current-phase authenticated domain for:
/sign-in
/portal
/admin
/superadmin
- Tenant public domains remain browsable without auth.
- Auth-required entry points on tenant public domains redirect to central sign-in with:
- tenant slug when known
- originating host when needed for custom-domain resolution
- safe internal
returnTo path
- After auth,
/auth/complete resolves the tenant hint, stores it safely, and redirects to the intended central-domain flow.
- Current phase is central-auth only. Future Clerk satellite-domain rollout should reuse the domain helpers instead of scattering host assumptions across UI code.
- commission capture
- split settlement records
- payment request
PAID transitions
Live vs Prepared
- Live now in code:
- placeholder payment creation
- request persistence and visibility
- tenant-configurable verification thresholds
- cron-safe automation route structure
- reconciliation updates from webhook into payment requests
- Still requires live credentials/infrastructure to fully verify:
- Paystack hosted checkout and transfer-account responses
- webhook delivery
- Resend email delivery
- Inngest or external cron execution against the internal automation route
Pilot Readiness
Payment Authority Model
- Checkout initialization and admin-created payment requests are intent only.
- Buyer-facing
POST /api/payments/verify is read-only and non-authoritative.
POST /api/webhooks/paystack remains the source of truth for:
- payment
SUCCESS
- receipt creation
- transaction payment-state sync
- payment-request
PAID transition
- commission record upsert
- split-settlement record upsert
- Failed or expired provider events no longer generate receipts, commission records, or split-settlement records.
Split-Payment Caveats
- EstateOS computes split settlement from the active company commission rule and payout account readiness.
- Paystack-backed live checkout now refuses to initialize if split settlement is not ready for that tenant.
- Admin-created Paystack payment requests use the same settlement quote and split metadata path as buyer-initiated checkout.
- EstateOS does not fake provider settlement confirmation:
SplitSettlement.status = READY means the split payload was valid for initialization
- live subaccount settlement still requires real Paystack processing and payout configuration
- Granted-plan tenants still generate platform commission on successful payments.
Auth / Domain Model
- Public tenant browsing stays on the tenant site.
- Auth-required actions redirect to the central
PORTAL_BASE_URL.
- Production auth boundaries are enforced through Clerk plus server-side tenant/role guards.
- Dev bypass is now explicit:
- disabled by default
- available only outside production
- enabled only when
ESTATEOS_ENABLE_DEV_BYPASS=true
- The dev access switcher and
/api/dev/session are hidden/inert unless that flag is enabled.
Role Access Model
- Public visitor: tenant public pages only
- Buyer:
/portal
- Tenant admin:
/admin
- Superadmin:
/superadmin
- Buyer cannot inspect another buyer's payment verification state.
- Tenant admin remains tenant-scoped.
- Superadmin stays separate from tenant admin surfaces.
Required Production Environment
DATABASE_URL
DEFAULT_COMPANY_SLUG
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
CLERK_SECRET_KEY
PAYSTACK_SECRET_KEY
PAYSTACK_PUBLIC_KEY
PAYSTACK_WEBHOOK_SECRET
R2_ACCOUNT_ID
R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
R2_BUCKET_NAME
PORTAL_BASE_URL
PLATFORM_BASE_URL
CRON_SECRET if scheduled automation is enabled
RESEND_API_KEY and EMAIL_FROM for live transactional email
CLERK_WEBHOOK_SECRET for verified Clerk webhook ingestion
First-Tenant Onboarding Checklist
- Create the tenant company with correct slug, public domain, and billing settings.
- Configure the company payout account and confirm Paystack subaccount readiness.
- Confirm an active billing plan or approved grant exists for transaction operations.
- Publish tenant branding and public homepage content.
- Publish at least one verified property and one public marketer/team profile.
- Configure Clerk production keys and webhook endpoint.
- Configure Paystack callback URL and webhook URL.
- Configure R2 document storage and public asset base URL if used.
- Set
ESTATEOS_ENABLE_DEV_BYPASS=false in any shared staging or production environment.
Live Validation Checklist Before Pilot Launch
- Hosted checkout payment:
Create an admin payment request with
HOSTED_CHECKOUT, pay it through Paystack test mode, confirm webhook delivery, confirm the payment row flips to SUCCESS, confirm the payment request becomes PAID, and confirm the receipt appears in both admin and buyer views.
- Temporary transfer account request:
Create an admin payment request with
BANK_TRANSFER_TEMP_ACCOUNT, confirm Paystack returns transfer instructions, confirm they appear in buyer/admin UI, complete the transfer in test mode if supported, and confirm webhook reconciliation updates the request and payment records.
- Split-settlement readiness:
Remove the tenant payout subaccount or mark it inactive, attempt checkout, and confirm initialization is blocked with a clear operator error. Restore readiness and confirm initialization succeeds with split metadata.
- Buyer portal visibility:
Confirm amount paid, remaining balance, next due date, receipts, active payment requests, and marketer attribution all match the reconciled payment state.
- Admin monitoring:
Confirm
/admin/payments shows the same request/payment lifecycle, outstanding balance, receipt link, and marketer attribution.
- Superadmin revenue visibility:
Confirm successful payments produce commission records and split-settlement records visible through platform-owner billing views without exposing tenant-private data publicly.
- Auth and redirects:
From the tenant public site, click buyer portal, admin, and start-purchase CTAs and confirm they land on the central auth domain with the correct return target. Confirm invalid external redirect targets are rejected.
- Role boundaries:
Confirm buyer cannot access
/admin, tenant admin cannot access /superadmin, and tenant admin cannot cross tenant boundaries through IDs or redirect parameters.
Local Development Notes
- With Clerk configured, local auth uses real Clerk sessions.
- Without Clerk, local demo access works only when
ESTATEOS_ENABLE_DEV_BYPASS=true.
- If that flag is left
false, local auth-required routes will redirect to sign-in and require real auth configuration.