← Blog
Tenant Portal. Building What Vendors Won't

Tenant Portal. Building What Vendors Won't

nextjstypescriptprismaside-projectproperty-management

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