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. 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 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 # 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