Agency tools
Yui Tanaka9 min read5 views

Agency tools stack: client portal, billing, deliverables

Most agencies stitch together three or four SaaS subscriptions for a client portal, file delivery, and invoicing — and still email PDFs. You can replace all of it with one small app: Next.js, a managed data layer for projects and file deliverables, Stripe for invoicing, and role-based access so clients see only their own work. It's a weekend build that pays for itself in cancelled subscriptions. Here's the stack and the access-control trap that matters most.

A small agency team collaborating around a table in a warm, sunlit studio
A small agency team collaborating around a table in a warm, sunlit studio
On this page

Walk into most agencies and you'll find the same setup: one tool for the client portal, another for file delivery, a third for invoicing, and — somehow, still — PDFs going out over email. Each tool charges per seat, and the seats multiply with every client.

You can fold all of it into one small app. Here's the stack we'd build, and the one trap — access control — that you cannot get wrong.

What the portal actually needs

Scroll to see more

JobPick
App shell + authNext.js + Better Auth
Projects & deliverablesManaged data layer + file uploads
InvoicingStripe (recurring + one-off)
Access controlServer-side ownership checks
Client-facing UITailwind + shadcn/ui

Three purchased subscriptions collapse into one codebase you own and brand.

Why this stack

Projects and deliverables as real records

The core model is small: a client has projects, a project has deliverables, a deliverable is a file plus some status. A managed data layer gives you those records and the file uploads with signed URLs, so a client downloads a polished asset through a link that expires — not an email attachment.

Stack pick

One clientprojectdeliverable chain, linked by reference. Make these explicit related records, not loose fields. Every screen in the portal — the client's dashboard, your admin view, the invoice — is just a different query over the same three tables.

Invoicing belongs to Stripe

Agencies bill two ways: retainers and project fees. Stripe does both. Model each project with a reference to a Stripe customer, and let Stripe own the schedule, the dunning, and the receipts. You display status; you don't reimplement billing.

Access control is the whole game

Here's the part that matters more than anything else in this build:

A client portal is a promise that Client A can never see Client B's work. That promise is kept on the server or it isn't kept at all. Filter every deliverable query by the authenticated client's id, server-side. Hiding a button is decoration, not security.

Use the session user id on every read and write. The auth session exposes the logged-in user; resolve that to a client record and scope every query to it. A deliverable fetched by id must also match the owner, or it returns nothing.

Role-based views from one codebase

Your team needs an admin view; clients need their own slice. Same app, two roles. The admin sees all projects; the client sees only theirs. Branch on role at the data layer, not just the navigation, and you get a staff tool and a client tool from one deploy.

Wiring it together

 Client login (Better Auth)
      |
      v
  session.user.id ──▶ resolve client record
      |
      v
  Next.js /api: query projects WHERE owner = client
      |                         \
      v                          v
  deliverables (signed file URLs)  Stripe (invoices/status)

Stack risks

  • Broken access control. The single highest-stakes bug in a portal. Scope every query server-side and write a test that proves Client A gets a 404 for Client B's deliverable.
  • File link leakage. Signed URLs expire — use short lifetimes for sensitive deliverables and regenerate on demand rather than emailing permanent links.
  • Scope creep into a full PM tool. It's tempting to grow this into Asana. Resist. The portal's job is delivery and billing; keep project management wherever your team already lives.
  • Onboarding friction. A portal clients won't log into is worse than email. Make first login one click and the dashboard obvious.

Cost reality

One managed data-layer plan plus Stripe's per-transaction fee typically lands under what a single mid-tier portal SaaS charges per seat — and it doesn't climb with every client you add. The savings are real and they compound.

Your move:

Build the access-control check first — prove a logged-in client cannot fetch another client's deliverable — before you make a single screen look nice.

Sources

  • Better Auth documentation (2025) — sessions and role handling.
  • Stripe Invoicing & Subscriptions docs, Stripe (2026) — recurring and one-off billing.
  • OWASP guidance on broken access control (2025).
  • ShipGarden internal teardown notes, "agency client portal" (2026).
Y

Written by

Yui Tanaka

Frequently asked questions

Is it really worth building a portal instead of buying one?

If your needs are generic, buy. But agencies usually want their own branding, their own deliverable structure, and to stop paying per-seat fees that scale with every client. A small custom portal on a managed data layer often costs less per month than one mid-tier SaaS seat, and it's yours.

How do I keep one client from seeing another client's files?

Enforce ownership on the server, never in the UI. Every query for a deliverable must filter by the logged-in client's id on the server side. Hiding a link in the frontend is not access control — a guessed URL would still return the file unless the server checks ownership.

Do I need recurring billing or one-off invoices?

Most agencies need both: retainers (recurring) and project fees (one-off). Stripe handles both from the same dashboard, so model your projects to reference a Stripe customer and let Stripe own the payment schedule.