← Blog
Tenant Portal. Building What Vendors Won't

Tenant Portal. Building What Vendors Won't

nextjstypescriptprismaside-projectproperty-management

I manage a rental property. 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, and documents

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.

ploscope 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
  • Task Queue: Celery — async job processing for payment ingestion pipeline
  • Message Broker: RabbitMQ — job queue backend for Celery workers

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 and no webhooks. But I have a forwarding rule that routes every Zelle notification email to a sandbox inbox that an AI agent monitors. The agent parses the payment details and calls the portal’s REST API to log it automatically. I get a one-click approval prompt to confirm the data looks right before it’s committed.

The result: zero manual data entry, one approval click, full audit trail.

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. That said, it comes with real costs — you own the maintenance, the bugs, and the scope creep. I’ll dig into the full tradeoff in an upcoming post on build vs. buy decisions.

The code is open source: github.com/lcrostarosa/tenant-portal