Back to Blog
productportalclient-experience

Building a Client Portal That Replaced Email Chains

Ryan C·

Every agency eventually hits the same wall: project context is scattered across email threads, Google Drive links, Stripe receipts, and Slack channels. I built a client portal that puts everything behind one login — and the most common feedback I get is "I like that everything is in one place."

Here's how client communication used to work: I'd send a project update via email. The client would reply with feedback. I'd share a Google Drive link for deliverables. They'd ask for the invoice, and I'd forward the Stripe receipt. A week later, they'd email asking "where's that file you sent me?" and I'd dig through threads to find it.

Multiply this by eight clients and it becomes unsustainable. Not because any single interaction is hard, but because scattered context is the real productivity killer.

What the Portal Does

Every client gets a login (magic link — no passwords to manage) that gives them a single view of everything related to their projects:

Dashboard — Active projects with status indicators, recent activity feed, quick stats (file count, message count). The first thing they see after logging in is "here's where things stand."

Projects — Each project has its own page with:

  • Current status (discovery → planning → in progress → review → deployed → completed)
  • Message thread for async communication
  • File library organized by type
  • Link to the live site (if deployed)

Files — Every deliverable, contract, invoice, and asset in one place. Categorized by type, filterable by project. No more "can you resend that PDF?"

Billing — Active services, payment history, and a self-service Stripe portal link for managing subscriptions. No more forwarding receipts.

RBAC: Two Views, One Codebase

The portal serves two audiences with the same routes:

Clients see only their own projects, files, and billing. The sidebar shows Dashboard, Projects, Files, Messages, Billing, and Settings. Everything is scoped to their clientId.

Admins see everything, plus an admin section with client management, project controls, revenue dashboards, and conversation logs from the AI chatbot. The sidebar expands with admin-only navigation.

// Every portal query is scoped
const projects = user.role === 'admin'
  ? await prisma.project.findMany()
  : await prisma.project.findMany({
      where: { clientId: user.clientId }
    })

This keeps the codebase simple. I don't have separate admin and client apps. Same pages, same components, different data based on who's logged in.

File Management

Files were the biggest pain point. Clients would lose Google Drive links, email attachments would expire, and I'd waste time re-uploading the same deliverable.

Now every file lives in S3 with metadata in the database:

File
├── name: "brand-guidelines-v2.pdf"
├── type: "deliverable" | "documentation" | "asset" | "contract" | "invoice"
├── size: 2_450_000
├── storageKey: "projects/abc123/uploads/20260224-brand-guidelines-v2.pdf"
├── url: signed download URL
└── projectId → Project

Upload is drag-and-drop with progress tracking. Files are capped at 50MB each. The storage key includes the project ID for isolation — even if someone guesses a URL, the signed URL expires and the access check verifies project ownership.

File types matter for organization. When a client visits their Files page, deliverables are separate from contracts, which are separate from invoices. Small detail, but it answers "where's my invoice?" without a search.

The Activity Feed

The dashboard shows a unified activity feed combining messages and file uploads across all projects. For clients, this answers "what happened since I last checked?" without clicking into each project.

Today
  📎 Brand guidelines v2 uploaded to Website Redesign
  💬 "The homepage mockup is ready for review" in Website Redesign

Yesterday
  💬 "Updated the color palette based on your feedback" in Website Redesign
  📎 Revised sitemap uploaded to Website Redesign

For admins, the feed covers all clients and all projects — a bird's-eye view of where activity is happening.

Messages as a First-Class Feature

I considered integrating Slack or building a real-time chat. Both were overkill. Client communication in agency work is async by nature. Nobody needs real-time typing indicators when the response time is measured in hours.

Messages are simple: a text field, a send button, and a chronological thread scoped to a project. System messages (like "project status changed to Review") are flagged with isSystem: true and styled differently.

The key decision was making messages project-scoped, not client-scoped. A client with three projects has three separate message threads. This keeps context tight — a conversation about the website redesign doesn't bleed into the mobile app discussion.

What Clients Actually Say

The feedback I get most often is "I like that everything is in one place." That sounds trivial, but it's the whole point. The portal doesn't do anything that email + Google Drive + Stripe can't do. It just puts it all behind one login with one mental model.

The second most common feedback: "The magic link login is great." No passwords to remember, no "forgot password" flow. Click the link in your email, you're in. For clients who interact with the portal maybe twice a month, this is the right tradeoff.

What I'd Add Next

Notifications. Right now, clients don't know something happened unless they log in. Email notifications on new messages and file uploads would close the loop.

Mobile optimization. The portal works on mobile but isn't optimized for it. The sidebar should collapse to bottom tabs, and the file upload experience needs touch-friendly adjustments.

These are incremental improvements. The core value — centralizing project context into one place — is already delivering.

Want us to build something like this for you?

We ship production software in days, not months. Tell us what you need — our AI receptionist is standing by.

Back to Blog
Page

Page

Client AI · Online

Page

Hey, I'm Page.

Tell me what you need. I'll point you to the right person — or tell you if we're not the right fit.

Powered by Claude · Responses may vary