Next.js 16 builds with Turbopack, which does NOT stamp the middleware CSP nonce onto its inline hydration scripts (self.__next_f.push). The nonce-based `script-src 'self' 'nonce-…'` therefore blocked those inline scripts, React never hydrated, and the marketing/app pages rendered as a blank/black shell (header + framer-motion sections stuck at opacity:0). Switch `script-src` to 'self' 'unsafe-inline' (Turbopack-compatible) and drop the now-unused nonce plumbing. All other CSP directives stay strict (object-src 'none', frame-ancestors 'none', locked connect-src/frame-src). Verified in a local production container: served script-src is correct and the page hydrates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🏠 Property Management Network
The all-in-one property-management platform for independent landlords.
Properties, tenants, rent, maintenance, leases, expenses, AI insights, and integrations — in one clean dashboard.
✨ Overview
Property Management Network replaces the spreadsheet-and-WhatsApp chaos that most small landlords live with. It gives a solo landlord or a small team a single source of truth for their whole portfolio — and the automation, AI, and integrations to run it hands-off.
Everything is multi-tenant and team-aware: each landlord operates on their own isolated portfolio, and Landlord/Lifetime accounts can invite teammates with scoped roles.
🧰 What you can do
Core operations
- 🏢 Properties & units — manage your whole portfolio with live occupancy tracking and a map view (addresses are auto-geocoded).
- 👥 Tenants — profiles, lease history, payment records, and a private tenant portal (token-based, no login required).
- 💵 Rent tracking — log payments, send Stripe payment links, and auto-mark balances overdue with automatic late fees.
- 🔧 Maintenance — full status workflow (Open → In Progress → Resolved), with tenant-submitted requests from the portal.
- 📄 Leases — expiry countdowns, automated 60/30/7-day email alerts, and e-signature (DocuSign / Dropbox Sign).
- 🧾 Expenses — categorized logging with recurring-expense support.
- 🗂️ Documents — a per-property file vault stored in object storage and served through an auth-gated route.
- 🔎 Inspections & vendors — move-in/out/routine inspection checklists and a vendor directory.
- 📊 Reports & exports — portfolio analytics with CSV export.
- 📅 Calendar — an in-app calendar plus a read-only iCal (ICS) feed you can subscribe to.
Automation & AI
- 🤖 AI features — recommendations, predictions, impact tracking, and a portfolio assistant (OpenAI). (Pro and up.)
- ✉️ Automated email — rent reminders, overdue notices, and lease-expiry alerts, plus a configurable follow-up engine.
- 🎨 White-label branding — put your own brand on the tenant portal. (Landlord / Lifetime.)
- 🛡️ Admin dashboard — superadmin tools with a full audit log.
🔌 Integrations & developer platform
| Capability | Details |
|---|---|
| 🌐 Public REST API | Versioned /api/v1 endpoints (properties, tenants, payments, maintenance, webhooks) authenticated with Bearer API keys. See /api-docs. |
| 🪝 Outbound webhooks / Zapier | Subscribe to events (tenant.created, payment.paid, maintenance.updated, …). Deliveries are HMAC-signed, retried with backoff, and Zapier-compatible via the REST-hook subscribe/unsubscribe pattern. |
| 💳 Payments | Stripe (subscriptions + rent payment links). |
| 📚 Accounting sync | One-way push of income & expenses to QuickBooks Online or Xero (OAuth). |
| ✍️ E-signature | Send leases for signature via DocuSign or Dropbox Sign. |
| 🔑 Auth | Email/password and Google OAuth (Better Auth). |
Every integration is env-gated: unconfigured providers show a clean “not configured” state instead of a broken button.
💳 Plans & pricing
| Plan | Price | Highlights |
|---|---|---|
| 🆓 Starter | Free | 1 property, 3 tenants, no AI |
| 🚀 Pro | $29/mo | 10 properties, unlimited tenants, AI (50 calls/mo) |
| 🏆 Landlord | $59/mo | Unlimited properties, team access, white-label, AI (200/mo) |
| ♾️ Lifetime | $199 once | Everything in Landlord, forever |
Billing runs through Stripe. Products/prices are resolved by stable lookup keys and auto-created on first checkout, so going live is just an API-key swap — no price IDs to wire up.
🧱 Tech stack
| Layer | Technology |
|---|---|
| Framework | Next.js 16.2 (App Router, TypeScript, React 19) |
| Styling | Tailwind CSS + Geist |
| Database | PostgreSQL via Drizzle ORM |
| Auth | Better Auth (email/password + Google OAuth) |
| Object storage | DigitalOcean Spaces (S3-compatible, CDN, auth-gated) |
| Payments | Stripe |
| AI | OpenAI (gpt-4o-mini) |
| SMTP (SMTP2GO) | |
| Maps | Leaflet + OpenStreetMap / Nominatim geocoding |
| Cron | DigitalOcean Functions (scheduled triggers) |
| Deploy | DigitalOcean App Platform (Docker image via DOCR) |
🚀 Getting started
1. Clone & install
git clone <your-repo>
cd property-management-network
npm install
2. ⚙️ Configure environment
Copy the template and fill in your own values:
cp .env.example .env.local
.env.local holds your database URL, auth secret, and credentials for Stripe, OpenAI, SMTP, and object storage. Every variable is documented inline in .env.example, and the full production reference lives in DIGITALOCEAN.md. Never commit real secrets.
3. 🗄️ Run migrations
The schema is managed by Drizzle (see drizzle.config.ts). Point DATABASE_URL at your PostgreSQL instance, then:
npm run db:migrate # apply migrations
npm run db:generate # regenerate after schema changes
npm run db:push # push schema directly (quick local prototyping)
4. 🔌 Wire up services (as needed)
- Stripe — set the API keys, then add a webhook at
https://yourdomain.com/api/stripe/webhookforcheckout.session.completed, thecustomer.subscription.*events,invoice.payment_failed, andpayment_intent.succeeded. - Email — verify a sending domain with your SMTP provider (e.g. SMTP2GO) and set the
SMTP_*+EMAIL_FROMvars. - Google / OpenAI / accounting / e-sign — each is optional and activates once its env vars are present.
5. ▶️ Run locally
npm run dev
Open http://localhost:3000.
6. 🚢 Deploy
The repo ships a production Dockerfile (Next.js standalone), an App Platform spec at .do/app.yaml, DO Functions cron under functions/, and a /api/health probe. Follow DIGITALOCEAN.md for the full walkthrough.
🗂️ Project structure
app/
├── (marketing)/ # Landing page, pricing, legal, API docs
├── (auth)/ # Login, signup, password reset
├── (dashboard)/ # Auth-gated app (properties, tenants, rent, maintenance,
│ # leases, expenses, inspections, vendors, reports,
│ # calendar, AI, onboarding, settings)
├── (admin)/ # Superadmin dashboard
├── api/
│ ├── v1/ # 🌐 Public REST API (Bearer API keys)
│ ├── webhooks + cron/ # 🪝 Outbound webhook delivery + scheduled jobs
│ ├── stripe/ # 💳 Billing + payment links + provider webhooks
│ ├── integrations/ # 📚 QuickBooks / Xero OAuth
│ ├── esign/ # ✍️ DocuSign / Dropbox Sign
│ └── … # Properties, tenants, rent, maintenance, documents, AI
└── tenant-portal/[token]/ # Public tenant portal (no login)
lib/
├── db/ # Drizzle schema, queries, migrations
├── auth.ts account.ts # Better Auth + team/account scoping
├── storage.ts # Object storage (Spaces) with local-disk dev fallback
├── webhooks/ # Event catalog, HMAC signing, SSRF guard, delivery
├── stripe/ # Billing clients & plans
├── accounting/ esign/ # QuickBooks/Xero & DocuSign/Dropbox Sign
├── ai/ # OpenAI client + prompts
├── email/ # SMTP (SMTP2GO) client + HTML templates
└── validations/ # Zod schemas for all entities
🗄️ Data model & isolation
The schema spans ~30 tables managed via Drizzle ORM, grouped roughly as:
- Core —
profiles,properties,units,tenants,rent_payments,maintenance_requests,leases,expenses,documents,inspections,vendors - Automation & AI —
notifications,follow_up_rules,follow_up_log,ai_recommendations,ai_predictions,activity_log,usage_events - Accounts & platform —
account_members,api_keys,app_settings,admin_audit_log,accounting_connections,signature_requests,webhook_endpoints,webhook_deliveries - Auth (Better Auth) —
user,session,account,verification
🔐 Tenancy is enforced in the application layer. Every query scopes by the resolved account owner id (team-aware), never the raw session user. There is no database RLS, so this scoping must be preserved on every new route — see
lib/account.ts(getEffectiveOwnerId).
⏰ Scheduled jobs
Cron is driven by DigitalOcean Functions hitting CRON_SECRET-protected endpoints (functions/project.yml):
| Job | Schedule (UTC) | What it does |
|---|---|---|
daily |
09:00 | Rent reminders, overdue marking, 60/30/7-day lease-expiry alerts |
late-fees |
08:00 | Applies late fees past the grace period |
follow-ups |
10:00 | Runs each account's active follow-up rules |
webhooks |
every 5 min | Retries pending outbound webhook deliveries |
🔒 Security highlights
- 🔑 API keys are stored as SHA-256 hashes; the plaintext is shown once.
- 🪝 Webhook payloads are HMAC-SHA256 signed (
X-PMN-Signature); endpoint URLs are SSRF-guarded (private/loopback/metadata ranges blocked). - 📁 Uploaded files are served only through an auth-gated route; object storage is required in production (uploads fail loud rather than silently hit ephemeral disk).
- 🛢️ Verified TLS to Postgres in production (
DATABASE_SSL=require+ CA). - ⏱️ Cron endpoints use a constant-time bearer check and fail closed.
📜 License
Proprietary — © 2026 Property Management Network. All rights reserved.
This source code is proprietary and confidential. No license or permission is granted to use, copy, modify, merge, publish, distribute, sublicense, or sell any part of it without the prior written consent of the copyright holder. See LICENSE.
