Files
property-management-network/README.md
T

225 lines
8.2 KiB
Markdown
Raw Normal View History

<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