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>
225 lines
8.2 KiB
Markdown
225 lines
8.2 KiB
Markdown
<p align="center">
|
|
<picture>
|
|
<source media="(prefers-color-scheme: dark)" srcset="public/logo-light.png">
|
|
<img alt="Property Management Network" src="public/logo-dark.png" width="360">
|
|
</picture>
|
|
</p>
|
|
|
|
# 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](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
|
|
|
|
```bash
|
|
git clone <your-repo>
|
|
cd property-management-network
|
|
npm install
|
|
```
|
|
|
|
### 2. Configure environment variables
|
|
|
|
```bash
|
|
cp .env.example .env.local
|
|
```
|
|
|
|
Fill in `.env.local`:
|
|
|
|
```env
|
|
# 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`:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
npm run dev
|
|
```
|
|
|
|
Open [http://localhost:3000](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/app.yaml), DO Functions cron under [`functions/`](functions/), and a `/api/health` liveness probe. See **[DIGITALOCEAN.md](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
|