Files
property-management-network/README.md
T
Leon SerfatyandClaude Opus 4.8 c9968531e4 Consolidate audit-fixes branch: webhooks, integrations, and deploy hardening
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>
2026-07-02 13:42:34 -04:00

8.2 KiB

Property Management Network

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)
Email 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.completed
  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • invoice.payment_failed
  • payment_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