Property Management Network — Next.js 16 (App Router), Better Auth, Drizzle ORM over PostgreSQL, Stripe, OpenAI, Resend. Includes: - Security hardening: access-control/IDOR fixes, TLS-by-default DB layer, constant-time cron auth, strict security headers, atomic AI quota gating, HTML/email output encoding, demo-backdoor disabled in production. - Superadmin dashboard at /admin (overview/MRR, server-paginated users with ban/impersonate/plan/delete, billing, platform activity + admin audit log, AI usage, system health) via the Better Auth admin plugin. - Seed/migration utility scripts under scripts/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Property Management Network
Property management SaaS for independent landlords. Track properties, tenants, rent, maintenance, leases, and expenses — all in one clean dashboard.
Built with Next.js 16, PostgreSQL (Drizzle ORM), Better Auth, Stripe, and OpenAI. Ready to deploy on Vercel in under 10 minutes.
What it does
Property Management Network replaces the spreadsheet + WhatsApp chaos that most small landlords live with. Key capabilities:
- Properties & units — manage your entire portfolio with occupancy tracking
- Tenant profiles — contact info, lease history, payment records, and a private tenant portal
- Rent tracking — log payments, send Stripe payment links, auto-mark overdue balances
- Maintenance requests — status workflow (Open → In Progress → Resolved), tenant submissions via portal
- Lease management — expiry countdowns, automated 60/30/7-day email alerts
- Expenses — categorized logging with recurring expense support
- Documents — file vault per property with drag-and-drop upload to local disk, served through an auth-gated route
- AI features — AI-powered recommendations, predictions, and impact tracking (Pro+)
- Automated emails — rent reminders, overdue alerts, lease expiry notifications via Resend
- Tenant portal — token-based (no login), tenants can view rent history and submit maintenance
Revenue model
| Plan | Price | Limits |
|---|---|---|
| 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 one-time | Everything in Landlord, forever |
Subscription billing via Stripe. Lifetime deal is ideal for Flippa buyers who want to offer an LTD to early customers.
Tech stack
| Layer | Tech |
|---|---|
| Framework | Next.js 16.2 (App Router, TypeScript) |
| Styling | Tailwind CSS + Geist font |
| Database | PostgreSQL (via Drizzle ORM) |
| Auth | Better Auth (email/password + Google OAuth) |
| Storage | Local disk (auth-gated file serving) |
| Payments | Stripe (subscriptions + payment links) |
| AI | OpenAI (gpt-4o-mini) |
| Resend | |
| Cron | Vercel Cron Jobs |
| Deploy | Vercel |
Setup
1. Clone and install
git clone <your-repo>
cd property-management-network
npm install
2. Configure environment variables
cp .env.example .env.local
Fill in .env.local:
# Database (PostgreSQL via Drizzle ORM)
DATABASE_URL=
# Auth (Better Auth)
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=your-random-secret-string
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# File storage (local disk)
STORAGE_DIR=./storage
# Stripe
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
STRIPE_PRO_MONTHLY_PRICE_ID=
STRIPE_LANDLORD_MONTHLY_PRICE_ID=
STRIPE_LIFETIME_PRICE_ID=
# OpenAI
OPENAI_API_KEY=
# Resend
RESEND_API_KEY=
RESEND_FROM_EMAIL=Property Management Network <noreply@yourdomain.com>
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
CRON_SECRET=your-random-secret-string
3. Run database migrations
The schema is managed with Drizzle ORM (see drizzle.config.ts). Point DATABASE_URL at your PostgreSQL instance in .env.local, then apply the migrations from lib/db/migrations:
npm run db:migrate
To regenerate migrations after changing the schema, use npm run db:generate. For quick local prototyping you can push the schema directly with npm run db:push.
4. Configure Stripe
Create three products in your Stripe dashboard:
- Pro Monthly — $29/mo recurring → copy Price ID to
STRIPE_PRO_MONTHLY_PRICE_ID - Landlord Monthly — $59/mo recurring → copy Price ID to
STRIPE_LANDLORD_MONTHLY_PRICE_ID - Lifetime — $199 one-time → copy Price ID to
STRIPE_LIFETIME_PRICE_ID
Set up a webhook at https://yourdomain.com/api/stripe/webhook listening to:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_failedpayment_intent.succeeded
5. Configure Resend
Add a verified sending domain in your Resend dashboard. Update RESEND_FROM_EMAIL with your domain address.
6. (Optional) Google OAuth
Create OAuth credentials in the Google Cloud Console and set GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET to enable Google sign-in via Better Auth.
7. Run locally
npm run dev
Open http://localhost:3000.
8. Deploy to Vercel
Connect the repo in the Vercel dashboard and add all environment variables under Settings → Environment Variables.
Cron jobs are pre-configured in vercel.json and run automatically on Vercel.
Project structure
app/
├── (marketing)/ # Landing page, pricing, legal
├── (auth)/ # Login, signup, password reset
├── (dashboard)/ # All dashboard pages (auth-gated)
│ ├── dashboard/ # Overview + stats
│ ├── properties/ # Property + unit management
│ ├── tenants/ # Tenant profiles
│ ├── rent/ # Payment tracking
│ ├── maintenance/ # Maintenance requests
│ ├── leases/ # Lease tracking
│ ├── expenses/ # Expense logging
│ └── settings/ # Billing + profile
├── api/
│ ├── properties/ # CRUD
│ ├── tenants/ # CRUD + auto unit assignment
│ ├── rent/ # CRUD + Stripe payment links
│ ├── maintenance/ # CRUD + status workflow
│ ├── leases/ # CRUD
│ ├── expenses/ # CRUD
│ ├── documents/ # Document metadata (files on local disk)
│ ├── ai/ # Rent receipts + maintenance summaries
│ ├── notifications/ # Send emails via Resend
│ ├── stripe/ # Checkout, portal, webhook
│ └── cron/ # Rent reminders + lease expiry alerts
└── tenant-portal/[token]/ # Public tenant portal (no login)
lib/
├── db/ # Drizzle schema, queries, migrations
├── auth.ts # Better Auth config
├── storage.ts # Local-disk file storage helpers
├── stripe/ # Client, plans, payment links
├── ai/ # OpenAI client + prompts
├── email/ # Resend client + HTML templates
└── validations/ # Zod schemas for all entities
drizzle.config.ts # Drizzle ORM config (DATABASE_URL, migrations dir)
Database schema
11 tables, managed via Drizzle ORM:
profiles · properties · units · tenants · rent_payments · maintenance_requests · leases · expenses · documents · notifications · usage_events
Data isolation is enforced in the application layer: every API route authenticates via getSessionUser() and scopes its queries by user_id. There is no database-level RLS, so this query scoping must be maintained carefully on every new route and query.
Cron jobs
| Job | Schedule | What it does |
|---|---|---|
| Rent reminders | Daily 9am UTC | Marks overdue payments, sends 3-day reminder emails |
| Lease expiry | Daily 10am UTC | Sends 60/30/7-day expiry alerts to landlord |
Cron routes are protected with CRON_SECRET (Bearer token in Authorization header).
License
MIT