Case Study / Build 08
Client Portal AI Build
A secure consultancy client portal bringing together requests, retained hours, documentation, invoices, and delivery evidence in one simple workspace.
Greg's take
A support portal for client retainers sounds simple until it has to handle requests, shared visibility, billing context, documentation, identity, audit trails, and a reliable data layer.
Most teams solve those pieces with separate tools. The result is a client relationship managed across disconnected systems with no single view.
The production system now runs on a Cloudflare-native architecture: Pages, Access, Workers, D1, and R2. It gives live customers a simple workspace without Jira or Zendesk complexity.
Build milestones
Version 1 production delivery is complete.
Secure Client Portal
Cloudflare Access authentication
Client dashboards
Requests
Threaded comments
Request attachments
Documentation library
Document downloads
Invoice history
Invoice downloads
Retainer tracking
Billing
Activity timeline
Admin portal
Cloudflare D1
Cloudflare R2
Tenant isolation
Production hardening
Usage Evidence Layer
Governance Pack presentation
Admin audit export
Client analytics
The problem
Support managed by email. No shared visibility.
No client visibility
Clients had no shared view of open requests, progress, or history. Every status update required an email exchange.
No billing transparency
Hours logged, budget consumed, and remaining retainer balance were only visible in spreadsheets that had to be shared manually.
No documentation layer
SOPs, process notes, and technical references existed in files and Notion - not in a client-facing, versioned system.
No shared client instance
Multiple stakeholders from the same client organisation had no shared workspace to see common requests and shared resources.
02 / The system
A business system. Not a portal UI.
The system was built to provide genuine operational value - shared visibility, structured data, Cloudflare Access authentication, tenant isolation, durable documentation, and operational controls - not just a frontend that looks like a portal.
Multi-client shared instance model
Everyone attached to the same client account sees the same requests, status updates, billing summary, documentation, and downloads. No separate admin panel required to share visibility.
Request lifecycle with structured status
Requests move through defined states: submitted, in review, in progress, blocked, completed. Each state change is recorded. Comments are threaded against requests, not floating in email.
Billing and hours visibility
Hours logged against requests are visible to clients in context. Retainer balance, hours consumed, and hours remaining are surfaced at the account level without a finance email.
Documentation system
Durable documentation with metadata and body endpoints. SOPs, technical references, process notes, and file assets live in a client-specific workspace rather than being scattered across email, shared drives, Jira, and Confluence.
Cloudflare Access authentication
Client access stays simple while the security layer remains enterprise-grade: Cloudflare Access, JWT validation, client-specific visibility, and no heavy enterprise tool rollout.
Admin controls and audit logging
Admin view with full request management, status override, hours adjustment, and activity log. Every change is timestamped and attributed.
Product philosophy
Why we built it differently.
Most client portals focus on tickets. This portal focuses on the consultancy relationship.
Clients can immediately see remaining hours, current requests, documentation, invoices, and recent activity without learning Jira, Zendesk, or another heavy delivery platform.
The goal is low operational overhead: enough structure to make delivery visible, with none of the complexity that turns a support workspace into a system clients have to be trained to use.
How it works
Cloudflare-native production stack with low operational overhead.
System architecture
Cloudflare Pages
Production frontend hosting
Cloudflare Access
Authentication, JWT issuance, access control
Workers API
Requests, comments, hours, documents, invoices, admin, activity
Data and file layer
Cloudflare D1
Structured records and tenant data
Cloudflare R2
Attachments and document downloads
Frontend
API
Database
File storage
Identity
04 / Delivery progression
From browser prototype to production platform.
The original browser prototype was replaced by the production Cloudflare platform in controlled slices. Each completed slice had acceptance criteria and a QA gate before live customer use.
Static prototype - localStorage
CompletedFull UI built on localStorage. All request, comment, billing, and documentation functionality working in-browser. No backend dependency. Rapid validation of the UX model and data structure.
Cloudflare Access / JWT identity layer
CompletedCloudflare Access added as the identity provider for portal.greg-staunton.com. JWT validation middleware built into Workers. Test auth flows verified before any data APIs were wired.
D1 database schema and base Workers API
CompletedD1 schema defined for requests, comments, hours, users, and documents tables. Base Workers API with routing and JWT middleware. Data migration from localStorage state verified in test environment.
Request and comment migration to D1/API
CompletedRequest creation, status changes, and threaded comments moved from localStorage writes to D1 API writes. Frontend updated to use fetch() against Workers API with JWT in headers. QA pass on create, read, update, and delete paths.
Hours logging and documentation endpoints
CompletedHours logging against requests moved to API/D1. Documentation metadata and body endpoints added. Client-facing billing summary calculated server-side and returned via API rather than computed from localStorage.
Production hardening pass
CompletedHardening pass on portal.greg-staunton.com: test auth bypass removed, JWT expiry handling, error states, loading states, empty states, and edge case handling across all request flows.
R2 file attachment foundation
CompletedCloudflare R2 added as the object storage layer for request attachments and document assets. D1 remains the structured database for metadata, users, requests, comments, billing, and audit records.
Admin audit export, evidence, governance, and analytics
NextThe remaining roadmap is limited to the Usage Evidence Layer, Governance Pack presentation, Admin audit export, and Client analytics.
05 / AI-assisted delivery
How AI was used in this build.
AI assistance was used throughout, but with clear human decision points at architecture, QA, and deployment. The Tanya Build Cockpit provided the structured scaffolding that made each session productive.
Architecture design
AI used for architecture option comparison: localStorage-only vs. D1-backed vs. full Cloudflare stack. Trade-offs documented. Human decision made before build started.
API implementation
Workers API routes generated with AI assistance using structured implementation packets - each route specified with method, auth requirement, request shape, and response contract before code generation.
D1 schema and migration
Schema design reviewed against the data model and access patterns before finalisation. Migration scripts generated and verified in test D1 environment before production deployment.
QA and hardening
AI used to generate QA checklists for each build slice, then human-run against the actual system. Issues found were fixed before the slice was marked complete.
06 / Production hardening
What was hardened before production launch.
The hardening pass addressed the specific gaps between a working browser prototype and a production system live customers can rely on.
Test auth bypass removed
Development-only authentication shortcuts removed before portal.greg-staunton.com was opened to actual clients.
JWT expiry handling
Expired or invalid JWT tokens result in a clear session refresh prompt rather than silent failure or broken state.
Error states on every API call
All fetch() calls have explicit error handling. No silent failures. Operators see a clear error message when an API call does not succeed.
Loading states throughout
Every async operation shows a loading indicator. Buttons disable while submission is in progress to prevent duplicate requests.
Empty states for all views
Every list or data view handles the empty case - no blank screens when there are no requests, comments, or documents yet.
Org-level data isolation verified
Each API request validates that the requesting JWT org_id matches the resource org_id. Cross-client data access not possible.
Evidence / Governance
A real business system with real operational constraints.
The Client Portal is the most complex build in the pl8ypus portfolio. It is a production system with live customers using it.
It shows multi-layered system thinking
Identity, API, database, file storage, admin controls, audit logging, and frontend UI all have to work together. The architecture decisions at each layer have to account for the layers above and below.
It shows controlled migration discipline
The successful move from the original localStorage prototype to Cloudflare D1 and R2 shows the same migration discipline needed inside real customer environments. Big-bang rewrites are not an option with live users.
It shows AI-assisted delivery at scale
A system of this complexity - six build slices, multiple data models, Cloudflare infrastructure, identity layer - was built with AI assistance throughout. The Tanya Build Cockpit system was the operational scaffold that made it possible.
It is honest about its current state
R2 object storage, D1 records, Access authentication, tenant isolation, and production hardening are complete. The remaining roadmap is limited to evidence, governance, audit export, and analytics layers.
Real client workflow
Built around Greg's real support and retainer model.
The Client Portal is not a portfolio piece designed to look like a real system. It is built around Greg's real support and retainer workflow - the one used with actual clients. The scope, the feature set, and the data model came from real operational needs, not from a case study brief.
That means the portal handles real requests, real documentation, real billing context, invoice downloads, shared account visibility for real client contacts, and the Cloudflare infrastructure that supports it in production, including D1 for structured records and R2 for file/object storage.
Requests come from real clients
The request lifecycle - submit, in review, in progress, blocked, completed - was designed around how Greg's client retainer support actually works, not a hypothetical workflow.
Billing context is real
Hours logged, retainer balance, and consumed time are surfaced for real client accounts - not placeholder numbers in a mockup designed to look believable.
Documentation is real
SOPs, process notes, and technical references served through the portal are the same documents that would otherwise live in email attachments and shared drives.
Multiple client contacts on the same account
The shared client instance model was designed because real client organisations have more than one person who needs visibility. That constraint shaped the data model from day one.
Delivery was driven by real operational pressure
Each production slice was prioritised by what a real client workflow actually needed - not what made a good portfolio story. Request writes came first because that is what clients use most.
Hardening was done for real users
The production hardening pass on portal.greg-staunton.com - JWT expiry handling, error states, empty states, org-level data isolation - was done because real users would encounter these paths.
Greg's take
A client portal sounds simple until it has to behave like a real support system. Requests, updates, documents, billing visibility, attachments, admin controls, audit trails, identity, and multiple people from the same organisation who all expect to see the same data.
That is where the toy version breaks. The toy version looks good in a screenshot, handles one user on a good day, stores everything in localStorage, and has no plan for when the session expires, the attachment needs to persist, or someone from finance asks why their hours do not match the invoice.
This build started as a lightweight prototype and moved slice by slice into a Cloudflare-backed production system. Each slice had acceptance criteria. Each delivery step had a verification gate. The point was not to fake production. The point was to earn it - one layer at a time, with the system working at every step of the journey.
The test of a portal is not whether it looks professional in a screenshot. The test is whether a real client with a real question can find their answer without sending an email. That is the bar this build was designed to clear.
Production evidence
Capabilities shown by this build.
Architecture
- Cloudflare-native stack design (Pages + Workers + D1 + R2 + Access)
- Multi-tenant data model with org-level isolation
- JWT-based identity with middleware validation pattern
Build discipline
- Slice-by-slice delivery with working state at every step
- Acceptance criteria defined before each production slice starts
- Production hardening pass as a distinct build phase, not an afterthought
AI-assisted delivery
- AI used at architecture, implementation, QA, and documentation layers
- Structured prompt patterns and project memory maintained across sessions
- Human decision points at every architectural and deployment choice
Operator readiness
- Non-technical clients can manage requests and view billing without training
- Admin controls and audit log designed for operational oversight, not developer use
- System state honest and visible - no hidden complexity or magic behaviour
Interested in a similar build?
For enterprise portal builds, Cloudflare infrastructure, applied AI systems, or marketing operations workflow design.