Tenant Portal. Building What Vendors Won't
I manage a few rental properties. My tenants pay rent via Zelle. My property management vendor handles the basics. but when I asked about API access to automate billing, track payments, or integrate with anything, the answer was: “We don’t have an API.” When I asked if they’d build one: “That’s not on our roadmap.”
Cool. I’ll build it then.
The Problem
Property management has a lot of repetitive work:
- Generating rent charges every month for every active lease
- Tracking payments that come in through Zelle (which has no webhook or notification API either)
- Coordinating maintenance. tenants text, I text a contractor, nobody has a paper trail
- Lease management. tracking dates, amounts, documents across multiple properties
My vendor’s software handles some of this, but it’s a closed box. No API means no automation. No automation means I’m manually entering the same data every month. And when tenants ask “did you get my payment?” I’m digging through Zelle transaction history.
I wanted a system where:
- Charges generate automatically on the 1st
- I record payments and they allocate to the right charges
- Tenants can see their own balance and submit maintenance requests
- Everything is accessible via API so I can wire it into other tools
The Solution
A full-stack tenant portal built with Next.js, TypeScript, and PostgreSQL. Two interfaces: a landlord dashboard for me, and a tenant portal for my renters.
Architecture
graph TD
subgraph Frontend
A[Landlord Dashboard]
B[Tenant Portal]
end
subgraph Next.js App Router
C[Server Actions]
D[REST API /api/v1/]
E[Auth - NextAuth v5]
end
subgraph Services
F[Data Access Layer]
G[Billing Service]
H[Maintenance Service]
I[Messaging Service]
end
subgraph External
J[Twilio SMS]
K[Resend Email]
L[Cron Endpoint]
end
subgraph Data
M[(PostgreSQL)]
N[Prisma ORM]
end
A --> C
B --> C
C --> E
D --> E
E --> F
F --> N --> M
G --> F
H --> F
I --> F
I --> J
H --> K
L --> G
L --> H
Data Flow: Monthly Billing
sequenceDiagram
participant Cron
participant API as REST API
participant Billing as Billing Service
participant DB as PostgreSQL
participant Tenant as Tenant Portal
Cron->>API: POST /api/v1/charges/generate
API->>Billing: Generate monthly charges
Billing->>DB: Find active leases
DB-->>Billing: Lease records
Billing->>DB: Create RENT charges (due 1st)
Note over Billing: One charge per active lease
Tenant->>API: GET /api/v1/charges?status=due
API->>DB: Query outstanding charges
DB-->>Tenant: Balance + charge details
Note over Tenant: Tenant pays via Zelle
API->>DB: Record payment, allocate to charge
DB-->>Tenant: Updated balance (paid)
Features
For me (landlord):
- Property and unit management with occupancy tracking
- Automated rent charge generation across all active leases
- Payment recording with multiple methods (Zelle, check, bank transfer, cash)
- Expense tracking by category. repairs, mortgage, insurance, utilities, taxes, HOA
- Mileage logging for property visits (tax deductions)
- Maintenance request management with priority and status workflows
- In-app messaging + optional SMS via Twilio
- Full REST API for external integrations
For tenants:
- View lease details, billing history, and current balance
- Submit and track maintenance requests
- Comment thread on maintenance items
- Message the landlord directly
The REST API is the whole point. Every entity. properties, units, tenants, leases, charges, payments. has full CRUD endpoints at /api/v1/. API key auth. This means I can automate anything: generate charges from a cron job, record payments from a script that watches my bank notifications, push maintenance updates to Slack.
Tech Stack
- Framework: Next.js 14 (App Router). server components, API routes, one deployment
- Language: TypeScript. type safety across the full stack
- Database: PostgreSQL + Prisma 7. relational data, great migrations, type-safe queries
- Auth: NextAuth v5. JWT strategy, role-based (landlord vs tenant)
- UI: Tailwind + shadcn/ui. fast to build, consistent, dark mode free
- Validation: Zod + React Hook Form. schema-first validation, shared between client and server
- SMS: Twilio (optional). tenant communication, inbound webhook for replies
- Email: Resend (optional). maintenance status notifications
- Testing: Vitest covering services and routes
I went with Next.js because I wanted one deployable unit. no separate API server, no separate frontend build. Server actions handle form submissions, API routes handle external integrations, and the App Router keeps everything organized.
Prisma was a deliberate choice over raw SQL. The schema is the source of truth, migrations are version-controlled, and the generated client gives me type-safe queries everywhere. For a project this size, the tradeoff (slight abstraction overhead vs. developer speed) is worth it.
Interesting Challenges
The Zelle Problem
Zelle has no API. No webhooks. No way to programmatically know when a payment arrives. So payment recording is still manual. but at least now it’s one click in my portal instead of updating a spreadsheet and then texting the tenant to confirm.
The REST API opens the door for future automation: if I ever get bank transaction notifications (via Plaid or similar), I can auto-match incoming Zelle payments to outstanding charges.
Role-Based Access
The landlord sees everything. Tenants see only their own data. NextAuth v5 with JWT strategy handles this. each request checks the user’s role and scopes queries accordingly. The tenant portal is a completely separate route group (/tenant/) with its own layout, so there’s no risk of accidentally exposing landlord views.
Billing Automation
Generating charges sounds simple until you handle edge cases: mid-month move-ins (prorate?), expired leases that shouldn’t generate, tenants with custom billing dates. The current implementation generates on active leases only, which covers 95% of cases. Proration is on the backlog.
Results
The portal has been running for my properties and it’s transformed the workflow. Monthly billing went from manual charge creation to a single cron trigger. Maintenance requests have a paper trail instead of living in a text thread. Tenants can check their own balance instead of asking me.
More importantly, I have an API. When my property management vendor eventually adds integrations (or I switch vendors), I can sync data both directions. The system is mine, the data is mine, and I can extend it however I want.
Sometimes the best solution to “the vendor won’t build it” is to just build it yourself.
The code is open source: github.com/lcrostarosa/tenant-portal
Related
- Why I Self-Host Everything: the homelab philosophy that makes running your own tenant portal feel normal
- Fulfilling a 3-Month Order Backlog at a Startup: another case of “the vendor won’t, so I will”