Initial import: property management SaaS + security hardening + admin dashboard
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>
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="public/logo-light.svg">
|
||||
<img alt="Property Management Network" src="public/logo-dark.svg" 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. 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) |
|
||||
| Email | Resend |
|
||||
| Cron | Vercel Cron Jobs |
|
||||
| Deploy | Vercel |
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
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`:
|
||||
|
||||
```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
|
||||
|
||||
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.completed`
|
||||
- `customer.subscription.created`
|
||||
- `customer.subscription.updated`
|
||||
- `customer.subscription.deleted`
|
||||
- `invoice.payment_failed`
|
||||
- `payment_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
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](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
|
||||
Reference in New Issue
Block a user