Batch commit of the pending working tree on security/audit-fixes-2026-07. Major areas: - Outbound webhooks / Zapier: schema + signed delivery with retries, public v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain. - Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when Spaces is unconfigured instead of silently using ephemeral disk. - Integrations & features (concurrent work): accounting (QuickBooks/Xero), e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding, expanded legal pages. - DB migrations 0006–0009. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.2 KiB
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. Deploys to DigitalOcean App Platform (see DIGITALOCEAN.md).
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 SMTP (SMTP2GO)
- 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 | DigitalOcean Spaces (S3-compatible, CDN, auth-gated) |
| Payments | Stripe (subscriptions + payment links) |
| AI | OpenAI (gpt-4o-mini) |
| SMTP (SMTP2GO) | |
| Cron | DigitalOcean Functions (scheduled triggers) |
| Deploy | DigitalOcean App Platform (Docker image via DOCR) |
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 (no price IDs needed — resolved by lookup key, auto-created on first checkout)
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
# OpenAI
OPENAI_API_KEY=
# Email (SMTP — e.g. SMTP2GO)
SMTP_HOST=mail.smtp2go.com
SMTP_PORT=2525
SMTP_USER=
SMTP_PASS=
EMAIL_FROM=postmaster@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
Add your API keys (STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) — that's it. Products and prices are resolved by stable lookup keys and auto-created on first checkout (Pro $29/mo, Landlord $59/mo, Lifetime $199, plus annual), so there are no price IDs to configure and going live is just an API-key swap. To pre-create the catalog, optionally run node scripts/stripe-setup.mjs.
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 email (SMTP)
Use any SMTP provider (e.g. SMTP2GO). Verify your sending domain with the provider, then set SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, and EMAIL_FROM.
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 (DigitalOcean App Platform)
The repo ships a production Dockerfile (Next.js standalone output), an App Platform spec at .do/app.yaml, DO Functions cron under functions/, and a /api/health liveness probe. See DIGITALOCEAN.md for the full walkthrough: build/push the image to DOCR, create the app, wire up Managed Postgres + Spaces, and deploy the scheduled cron functions.
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 SMTP (SMTP2GO)
│ ├── 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/ # SMTP (SMTP2GO) 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
