Four-Brain Consensus: Final Architecture Decision
All AI systems independently converged on the same core architecture.
Sterling (Claude Opus), Brad's Claude cross-check, ChatGPT 5.5, and Sterling's final reconciliation all agree: two completely separate production databases (employee vs. client), managed authentication via Clerk, one shared client database with strict tenant middleware, and one unified admin panel that orchestrates both systems.
ChatGPT 5.5 provided the critical correction on client-side isolation. The original recommendation of ~150 per-client D1 databases was over-engineering for purity. A single shared client database with the TenantScopedDB middleware pattern is the correct architecture for OA's scale and growth trajectory. Brad agrees with 95% of ChatGPT's evaluation.
What All Four Brains Agree On
- These are two different products, not two user types. The Client Portal and Employee Portal have different navigation, different data sources, different permission models, and different evolution trajectories. They happen to be operated by the same company. That's it.
- Two separate D1 databases (employee vs. client). Blast-radius containment. A breach of the client-facing surface physically cannot reach employee payroll, health insurance, or HR data. The connection does not exist at the infrastructure level.
- Managed auth provider (Clerk). Authentication is easy to get 95% right and catastrophic when the remaining 5% fails. Clerk is free for OA's scale (~900 total users) and SOC 2 certified.
- ONE shared client database with strict tenant architecture. Every table has a
company_idcolumn. Every query is automatically scoped through TenantScopedDB middleware. Developers never write raw queries. Cross-client leaks are structurally prevented at the application layer. - One unified admin panel (an orchestrator, NOT a third database). A single Worker at admin.outsourceaccess.com binds to both databases. OA staff push content to either portal from one interface. The admin panel is a bridge, not a data store.
- Authentication is not authorization. Separate logins do not prevent data leaks. Server-side authorization enforcement on every request, with identity derived from cryptographically signed JWTs (never from browser-sent parameters), is mandatory.
What Each Cross-Check Added
- Claude cross-check pushed harder on enforcement mechanics: "Show me endpoint by endpoint." "Include concrete test scenarios." "Don't tell me it's isolated, show me." This forced the detailed code patterns, authorization middleware, and GIVEN/WHEN/THEN test scenarios documented throughout this document.
- ChatGPT 5.5 added the "two different products" framing, emphasized long-term architectural evolution, and pushed back hard on per-client databases. Its key insight: "Optimize for the architecture you'll have at 5,000 clients, not just 150." At 5,000 clients, 5,000 separate databases is operationally untenable. The TenantScopedDB middleware pattern is battle-tested at massive scale by every major SaaS company.
- Sterling's reconciliation synthesized both cross-checks. The per-client DB approach was rejected in favor of ChatGPT's shared tenant model. The Claude cross-check's enforcement rigor was preserved. The result is a cleaner, more scalable architecture.
- All cross-checks correctly identified that cost-driven reasoning was clouding the original analysis. With cost explicitly off the table, the decision becomes purely about security, maintainability, and independent evolution.
Where the Original Recommendation Was Corrected
- Per-client D1 databases (~150 separate databases): REJECTED. ChatGPT 5.5 demonstrated that every schema migration, backup, reporting query, analytics query, AI model, and index must happen 150 times. At OA's growth target of 5,000 clients, this becomes 5,000 separate databases. Cross-client analytics becomes impractical. The TenantScopedDB middleware pattern already makes the shared approach nearly as secure, and it's the pattern used by every major SaaS company at scale.
- "Team dashboards" under Client Portal: Incorrect. Team dashboards are internal OA tools and belong on the Employee Portal side. Client Portal shows VA performance, industry research, resources, satisfaction history, and owner-only feedback. Fixed in this version.
- Cost as a factor: Sterling's original analysis weighed "simplicity" and "cost savings" in the architecture decision. Brad explicitly directed: cost is not a factor at OA's scale (~900 users). When cost is irrelevant, always choose the option that maximizes security, isolation, and independent evolution.
Final Architecture Diagram
graph TD
subgraph "Authentication Layer"
CK[Clerk - Managed Auth
Free up to 10K MAU, SOC 2]
end
subgraph "Application Layer"
CW[Client Worker
clients.outsourceaccess.com]
EW[Employee Worker
server.outsourceaccess.com]
AW[Admin Worker
admin.outsourceaccess.com
Orchestrator]
end
subgraph "Data Layer"
CDB[Client DB
oa-client-db
Shared tenant architecture
company_id on every table]
EDB[Employee DB
oa-employee-db
Payroll, HR, Benefits]
end
CK --> CW
CK --> EW
CK --> AW
CW --> CDB
EW --> EDB
AW -->|"Binding 1"| CDB
AW -->|"Binding 2"| EDB
style CK fill:#48A7DB,color:#fff,stroke:#0c4da2
style CW fill:#0c4da2,color:#fff
style EW fill:#362456,color:#fff
style AW fill:#7c3aed,color:#fff
style CDB fill:#059669,color:#fff
style EDB fill:#dc2626,color:#fff
"Optimize for the architecture you'll have at 5,000 clients, not just 150. Every schema migration, backup, reporting query, analytics query, AI model, and index must happen N times with per-client databases. At 5,000 clients, that's operationally untenable. The TenantScopedDB middleware pattern already makes the shared approach nearly as secure. Cross-client analytics becomes trivial with a shared DB (one query with GROUP BY) vs. querying 5,000 separate databases."
Detailed Sections
- Authentication vs. Authorization (The Core Distinction)
- Two Separate Production Databases (Employee vs. Client)
- Client Tenant Isolation (Shared DB with TenantScopedDB Middleware)
- Authentication Provider (Clerk)
- Cross-Domain VA Reference (Employees on Client Dashboards)
- Admin Panel as Orchestrator
- Concrete Test Scenarios
- Scale Architecture Principle
- Security Checklist
- Endpoint-by-Endpoint Authorization Map
Authentication vs. Authorization
Separate login pages do NOT mean data is isolated. Authentication answers "who is this person?" Authorization answers "what can this person access?" A system can authenticate someone perfectly and still hand them data they should never see if authorization is not enforced on every single request, server-side, without trusting anything the client sends.
Authentication = verifying identity. "This session belongs to user #472, who is a client_owner at ABC Landscaping." This happens once at login. Clerk handles this and issues a signed JWT.
Authorization = enforcing access rules on every request. "User #472 is requesting the performance dashboard for company #38. Is user #472 authorized to see company #38's data?" This happens on every single API call, server-side, by deriving the user's identity from the signed JWT and checking it against the database.
How Authorization Is Enforced: Endpoint by Endpoint
The server NEVER trusts a company_id, tenant_id, or user_id sent from the browser. Identity is always derived from the JWT, and permissions are checked against the database before any data is returned.
Key Principle: Server-Side Identity Derivation
- The browser sends a JWT (set by Clerk at login). That is the only piece of client-sent data we trust, because it is cryptographically signed and verified against Clerk's public key.
- From the verified JWT, we extract the user ID. From the user ID, we look up the company_id and role in our database.
- The company_id and role are NEVER sent by the browser. They are derived server-side, every time, on every request.
- If an attacker steals a session token, they can only access data for the company that token is associated with. They cannot pivot to another company's data because the company_id is bound to the session server-side.
- "We tag content by client" is NOT the answer. The answer is: we derive the company identity from a cryptographically verified JWT and use that derived identity as the sole filter on every database query, enforced automatically through the TenantScopedDB middleware.
Two Separate Production Databases: Employee vs. Client
Two completely separate D1 databases. Client data and employee data never share a store.
Employee HR data (payroll, health insurance, pay stubs) is a higher sensitivity class than client business performance data. A breach of the client portal must be structurally unable to expose employee payroll. Separate databases enforce this at the infrastructure level.
D1 Database: oa-employee-db
- ~400 OA employees
- Payroll records + pay stubs
- Health insurance enrollment
- Department/division KPIs for leaders
- HR announcements, holiday calendars
- Strategic initiatives (admin-pushed)
- Sensitivity: HIGH (PII, compensation, health)
D1 Database: oa-client-db (Shared Tenant Architecture)
- ~150 client companies today, scaling to 5,000+
- Every table has
company_idcolumn - VA performance dashboards (HubSpot)
- Time tracking (Time Doctor)
- Industry research, resources
- Satisfaction surveys, staff feedback
- Sensitivity: MEDIUM (business data)
Why Separate Databases (Not Just Separate Tables)
Blast-Radius Containment
- Infrastructure-level isolation: The client portal Worker binds to
oa-client-db. It physically cannot queryoa-employee-dbbecause it has no binding to it. A full remote code execution exploit in the client portal Worker still cannot reach employee payroll because the connection does not exist at the Cloudflare runtime level. - Different sensitivity classes: Employee compensation and health data may trigger compliance requirements (SOC 2, data residency, retention policies) that client business data does not. Separate stores let you apply different policies cleanly.
- Independent failure domains: A bad migration on the employee schema cannot lock or corrupt the client database. A D1 storage quota event on one cannot affect the other.
- Zero data overlap: No table in the employee database has a foreign key relationship to any table in the client database. They share no schema. Combining them into one database would be two databases sharing a container for no reason.
Client Tenant Isolation: ONE Shared Database with TenantScopedDB
One shared client database with strict tenant middleware. NOT 150+ per-client databases.
ChatGPT 5.5 pushed back hard on per-client databases, and Brad agrees. The TenantScopedDB middleware pattern makes cross-client leaks structurally impossible at the application layer. It is the same pattern used by every major SaaS company (Salesforce, HubSpot, Stripe, Shopify) and scales to thousands of tenants without operational overhead.
Why Per-Client Databases Were Rejected
The Operational Reality at Scale
- Schema migrations multiply. Every time a column is added or a table is changed, the migration must execute against 150 databases today, and 5,000 databases at OA's growth target. One migration failure in database #2,847 means a partially-migrated fleet that is painful to debug and reconcile.
- Backups multiply. 150 backup schedules. 150 retention policies. 150 restore procedures. At 5,000 clients, this becomes a full-time infrastructure job.
- Cross-client analytics becomes impractical. "What is the average satisfaction score across all clients?" With per-client databases, that means querying 150+ separate databases and aggregating the results. With one shared database, it's a single
SELECT AVG(score) FROM satisfaction_surveys GROUP BY company_id. - AI and reporting become painful. Training a model on aggregated client data, building reports across the portfolio, running business intelligence queries... all of this requires cross-database orchestration when data is split per-client. With a shared database, it's just SQL.
- Index tuning multiplies. Each database needs its own index analysis, performance monitoring, and optimization. At scale, this is a serious operational burden.
- ChatGPT's summary: "The per-client approach was over-engineering for purity when discipline-based isolation via middleware is battle-tested at massive scale."
The Recommended Architecture: TenantScopedDB Middleware
The key insight: developers never write SELECT * FROM performance. They write db.performance.for_company(current_company), where current_company comes from the authenticated session. The middleware makes it structurally impossible to forget the tenant filter.
Client Database Schema
All tables contain a company_id column. All queries are automatically scoped by the TenantScopedDB middleware. Here is the recommended schema from ChatGPT 5.5's evaluation.
| Table | Purpose | Key Columns |
|---|---|---|
Companies |
Master record for each client company | id, name, industry, plan_tier, onboarded_at, hubspot_company_id |
ClientUsers |
People who log in to the Client Portal | id, company_id, clerk_id, role, email, name |
VirtualAssistants |
VA assignments visible to client (display-only) | id, company_id, display_name, photo_url, role_title, start_date, employee_ref_id |
TimeDoctorMetrics |
Time tracking data pulled from Time Doctor API | id, company_id, va_id, date, hours_worked, productive_pct |
HubSpotMetrics |
Performance data pulled from HubSpot | id, company_id, va_id, period, metric_type, value |
Resources |
Industry research, playbooks, guides | id, company_id, title, type, industry_tag, content_url |
PerformanceHistory |
Historical performance snapshots for trend analysis | id, company_id, va_id, snapshot_date, satisfaction_score, kpi_data |
IndustryResearch |
Industry-specific insights and benchmarks | id, company_id, industry, research_type, content, published_at |
The TenantScopedDB middleware creates a structural barrier that is almost as strong as separate databases. Developers never touch the raw database binding. They only interact with request.scopedDb, which automatically scopes every operation to the authenticated company. A new developer cannot accidentally write a cross-tenant query because the raw DB is simply not available in handler scope. This is the same pattern Salesforce, Shopify, and HubSpot use across millions of tenants.
Authentication: Managed Provider (Clerk)
Brad's directive: cost is off the table. Evaluate on security alone.
Recommendation: Clerk (managed auth provider)
Authentication is the one component where getting it 95% right is worse than getting it 100% right. The 5% you miss (token rotation edge cases, session fixation, cookie SameSite behavior across browsers, brute-force protection, credential stuffing mitigation) are exactly the attack vectors that sophisticated attackers exploit. Clerk's team works on nothing but these problems. Sterling can build auth in 8 minutes, but Clerk's team has prevented auth bugs Sterling would never anticipate because they see millions of auth requests across thousands of apps.
Managed Auth (Clerk)
- Security surface area Sterling does NOT manage: Password hashing, salt generation, timing-safe comparison, bcrypt cost factor tuning, credential stuffing detection, brute-force rate limiting, CAPTCHA integration, session token rotation, refresh token revocation, cookie security flags (HttpOnly, Secure, SameSite), CSRF protection, OAuth flow security, JWT signing key rotation
- Compliance: SOC 2 Type II certified. They maintain the audit trail, not us.
- Multi-factor auth: Built in. TOTP, SMS, email magic links. We turn it on with a flag.
- Session management: Automatic token rotation, device tracking, session revocation from dashboard.
- SSO/SAML: Available if enterprise clients want to use their own identity provider. INFERRED Not needed today but eliminates a future rebuild.
- Free for the first 10,000 monthly active users. OA has ~900 total users. $0 for the foreseeable future.
Hand-Built Auth
- Security surface area Sterling DOES manage: All of the items listed to the left. Every one is a potential vulnerability if implemented imperfectly.
- Sterling builds fast, but auth bugs are subtle: A timing side-channel in password comparison leaks information. A missing SameSite=Lax flag enables CSRF. A refresh token that does not get rotated enables persistent session hijacking. These are not bugs you find in testing. They are bugs attackers find in production.
- Ongoing maintenance: When Chrome ships a new cookie policy (they do this regularly), when Apple changes WebKit session storage behavior, when a new class of session fixation attack is published, Sterling has to update. Clerk does this automatically.
- The "warranty" argument: Clerk is a specialist whose entire business depends on auth being correct. Hand-built auth has no warranty. Sterling IS the warranty, and every hour spent debugging an auth edge case is an hour not spent on revenue-moving work.
Clerk handles identity (login, session tokens, MFA). OA's D1 databases handle authorization (which company does this user belong to, what role do they have, what data can they see). Clerk's JWT tells us WHO. Our database tells us WHAT THEY CAN DO. The two are separate concerns, as they should be.
Cross-Domain Link: VAs on Client Dashboards
VAs are OA employees. Their names and performance metrics appear on client dashboards. This creates a cross-domain reference between the employee database and the client database. The solution: narrow reference, not database merging.
Approach: VA Summary in the Client Database (VirtualAssistants Table)
The client database contains a VirtualAssistants table with the minimal information a client needs to see: VA display name, profile photo URL, start date, and role. This is NOT a copy of the employee record. It is a purpose-built, minimal reference that contains only what the client portal needs to render the dashboard.
- What the client DB stores:
display_name,photo_url,start_date,role_title,employee_ref_id(opaque reference, not the employee's internal DB primary key). - What the client DB does NOT store: Payroll, health insurance, personal email, phone number, department assignments, KPI scores, HR records. None of that exists in the client database.
- Sync mechanism: A scheduled Worker job (Cron Trigger) reads minimal VA assignment info from the employee DB and writes the summary fields to the client DB. This runs on admin infrastructure (admin Worker, which has both bindings). INFERRED
- The employee_ref_id is opaque: It is a UUID or hash, not the employee's D1 primary key. Even if a client somehow obtained this ID, they could not use it to query the employee database because the client portal Worker has no binding to that database.
- Tenant-scoped: Like every other table, VirtualAssistants has a
company_idcolumn and is filtered by the TenantScopedDB middleware. Client A sees only their assigned VAs, never Client B's.
graph LR
subgraph EmpDB["oa-employee-db"]
E["employees table
Full record: name, email,
payroll, health, department"]
end
subgraph Sync["Admin Worker (Cron)"]
S["Copies ONLY:
display_name, photo_url,
start_date, role_title"]
end
subgraph ClientDB["oa-client-db"]
V["VirtualAssistants table
Minimal: display_name,
photo_url, start_date,
role_title, employee_ref_id
+ company_id (tenant scoped)"]
end
E -->|"Minimal fields only"| S
S -->|"Write summary"| V
style EmpDB fill:#1a1a2e,stroke:#dc2626,color:#fff
style ClientDB fill:#1a1a2e,stroke:#059669,color:#fff
style Sync fill:#48A7DB,stroke:#48A7DB,color:#fff
The employee database keeps the full record. The client database gets a purpose-built summary with only what the client dashboard needs. No database merging. No shared tables. No foreign key relationships across databases.
Admin Panel: An Orchestrator, NOT a Third Database
The admin panel is a bridge between two systems. It does not store its own data.
ChatGPT 5.5 nailed the framing: the admin panel at admin.outsourceaccess.com is a Cloudflare Worker that binds to BOTH the employee database and the client database. It can read from and write to either system. But it is not a third database. It is an orchestration layer that lets OA staff manage both portals from a single interface.
How the Admin Worker Works
- Dual D1 bindings: The admin Worker's
wrangler.tomlbinds to bothoa-employee-dbandoa-client-db. It is the only Worker with access to both. - Not a third database: Admin user accounts, roles, and permissions live in the employee database (since admins are OA employees). The admin panel does not have its own separate user store. It queries the employee DB to verify "is this person an OA admin?"
- Routing logic: Admin endpoints are prefixed by target system.
/api/admin/client/*routes queries tooa-client-db./api/admin/employee/*routes queries tooa-employee-db. The routing is explicit, never implicit. - Audit logging: Every write operation through the admin panel is logged with: who did it, what they changed, when, and on which system. This is critical for accountability when OA staff push content or modify records.
- Cross-system boundary: Clients never access the admin panel. Employees never access client data through the employee portal. The admin panel is the only place where both data sources are visible, and it requires OA admin credentials (not client or general employee credentials).
A third database would create a confusing question: "Where does this data live?" The answer should always be clear. Employee data lives in oa-employee-db. Client data lives in oa-client-db. Admin configuration and user roles live in oa-employee-db (since admins are employees). The admin panel just has the keys to both doors. It is a routing layer with authentication, not a data store.
Concrete Test Scenarios
Every isolation claim ships with a test. These are specific, runnable scenarios that prove the authorization model works.
Cross-Client Isolation Tests (Tenant Scoping)
Test 1: Client A attempts to fetch Client B's performance data by manipulating the URL
Test 2: Client A attempts to fetch Client B's record by guessing the record ID
SELECT * FROM satisfaction_surveys WHERE company_id = 38 AND id = 999. Returns 0 rows because survey 999 belongs to company 42, not 38. Response: 404 Not Found. DENIED - 404Test 3: Tenant isolation via TenantScopedDB middleware bypass attempt
request.scopedDb (scoped to company_id = 38)env.CLIENT_DB directly (the raw, unscoped binding)env.CLIENT_DB is not passed to handlers. Only the middleware has access to the raw binding. The handler's scope contains request.scopedDb and nothing else. The raw DB is structurally inaccessible from handler code. RAW DB NOT IN SCOPECross-Portal Isolation Tests
Test 4: Client session attempts to reach employee data
Test 5: Client employee (non-owner) attempts to access owner-only feedback
Test 6: Unauthenticated request to any API endpoint
Test 7: Forged JWT with a different company_id claim
Admin Panel Tests
Test 8: Non-admin user attempts to access admin panel
Scale Architecture Principle
"Optimize for the architecture you'll have at 5,000 clients, not just 150."
This was ChatGPT 5.5's core insight, and it changed the architecture recommendation. OA has ~150 clients today. The growth target is 5,000+. Every architectural decision should be evaluated against the 5,000-client scenario, not just the current state.
Shared Tenant DB at 5,000 Clients
- One schema migration, runs once
- One backup schedule, one retention policy
- One set of indexes to tune
- Cross-client analytics: single SQL query
- AI/ML training: one data source
- New client onboarding: one INSERT statement
- Operational complexity: O(1)
Per-Client DB at 5,000 Clients
- Schema migration runs 5,000 times
- 5,000 backup schedules to manage
- 5,000 sets of indexes to tune
- Cross-client analytics: query 5,000 DBs + aggregate
- AI/ML training: ETL from 5,000 sources
- New client onboarding: provision new DB + Worker binding
- Operational complexity: O(N) where N = client count
When Per-Client Databases WOULD Make Sense
Legitimate Reasons for Separate Databases
- Regulatory requirements: If specific clients are in regulated industries (healthcare with HIPAA, finance with SOX) that legally require physical data separation, per-client databases may be mandated.
- Contractual obligations: If enterprise clients negotiate contracts requiring dedicated infrastructure (their own database, their own encryption keys), per-client databases become a business requirement.
- Enterprise customers demanding it: Large clients with their own security teams may require proof of physical isolation as part of their vendor evaluation process.
- Extreme performance isolation: If one client's query patterns could starve others of database resources, physical separation prevents noisy-neighbor effects.
Why Shared Tenant Architecture Is Correct for OA
- No regulatory mandate: OA's client data (VA performance metrics, time tracking, satisfaction scores) is not subject to HIPAA, SOX, or similar physical-isolation requirements.
- No enterprise procurement requirements: OA serves SMBs ($1M-$300M revenue), not Fortune 500 companies with dedicated security review teams.
- Uniform data shape: Every client has the same schema (companies, users, VAs, metrics, resources). There is no per-client schema variation that would benefit from separate databases.
- Analytics is a core value proposition: OA will increasingly want to show clients how they compare to industry benchmarks, what top-performing clients do differently, and aggregate insights across the portfolio. This requires cross-client data access, which is trivial with a shared DB and painful with 5,000 separate ones.
- The TenantScopedDB middleware provides "good enough" isolation: It is not physically impossible for a bug to cross tenant boundaries (unlike separate databases). But the middleware makes it structurally very difficult. Combined with code review, testing, and the test scenarios in Section 7, the risk is acceptably low for OA's data sensitivity class (medium, not high).
"Is the additional security of physical database separation worth the operational cost at 5,000 clients, given that our client data is medium-sensitivity business metrics (not PII, not health data, not financial records)?" For OA, the answer is no. The TenantScopedDB pattern provides the right level of isolation for the data type, and it scales without operational overhead. The employee database (which IS high-sensitivity) remains physically separate. That boundary is absolute.
Reusable Security Protocol Checklist
This checklist applies to every portal, every endpoint, and every new feature added to the OA platform. It is the standing security contract.
Security Checklist (Every Endpoint, Every Feature)
Before any new endpoint or feature goes live, walk through all 6 items. For items 1-2, confirm the endpoint uses the authorization middleware and never reads IDs from the request. For item 3, confirm which isolation mechanism applies (physical separation for employee data, tenant middleware for client data). For item 4, confirm the new endpoint is deny-by-default. For item 5, count how many layers protect this data. For item 6, write the test scenario.
Endpoint-by-Endpoint Authorization Map
Every API endpoint, what database it touches, how identity is derived, and what authorization check runs.
Client Portal Endpoints (clients.outsourceaccess.com)
| Endpoint | DB | Identity Source | Authorization Check | Roles Allowed |
|---|---|---|---|---|
GET /api/client/performance |
oa-client-db | JWT → clerk_id → ClientUsers.company_id | TenantScopedDB filters by company_id | owner, manager, viewer |
GET /api/client/time-tracking |
oa-client-db | JWT → clerk_id → ClientUsers.company_id | TenantScopedDB filters by company_id | owner, manager, viewer |
GET /api/client/surveys |
oa-client-db | JWT → clerk_id → ClientUsers.company_id | TenantScopedDB filters by company_id | owner, manager |
GET /api/client/feedback |
oa-client-db | JWT → clerk_id → ClientUsers.company_id | TenantScopedDB + role = owner | owner ONLY |
GET /api/client/resources |
oa-client-db | JWT → clerk_id → ClientUsers.company_id | TenantScopedDB + industry tag filter | owner, manager, viewer |
POST /api/client/users/invite |
oa-client-db | JWT → clerk_id → ClientUsers.company_id | TenantScopedDB + role = owner | owner ONLY |
Employee Portal Endpoints (server.outsourceaccess.com)
| Endpoint | DB | Identity Source | Authorization Check | Roles Allowed |
|---|---|---|---|---|
GET /api/employee/payroll |
oa-employee-db | JWT → clerk_id → employees.id | Filter by server-derived employee_id (own records only) | All employees (own data) |
GET /api/employee/health-insurance |
oa-employee-db | JWT → clerk_id → employees.id | Filter by server-derived employee_id | All employees (own data) |
GET /api/employee/kpis |
oa-employee-db | JWT → clerk_id → employees.department_id | Filter by department_id + role >= team_leader | team_leader, ops_manager, admin, owner |
GET /api/employee/announcements |
oa-employee-db | JWT → clerk_id → employees.id | Public to all authenticated employees | All employees |
Admin Panel Endpoints (admin.outsourceaccess.com)
| Endpoint | DB | Identity Source | Authorization Check | Roles Allowed |
|---|---|---|---|---|
POST /api/admin/content/push |
Both (routed by path) | JWT → admin_users table (employee DB) | Admin role + target portal validation | OA admin staff only |
GET /api/admin/client/list |
oa-client-db | JWT → admin_users table (employee DB) | Admin role required | OA admin staff only |
POST /api/admin/employee/create |
oa-employee-db | JWT → admin_users table (employee DB) | Admin role + HR permission flag | OA HR admin only |
GET /api/admin/analytics/cross-client |
oa-client-db | JWT → admin_users table (employee DB) | Admin role + analytics permission | OA admin staff only |
GET /api/admin/audit-log |
Both | JWT → admin_users table (employee DB) | Admin owner role | Brad + senior admin only |
Every endpoint follows the same pattern: (1) JWT verified by Clerk, (2) user ID extracted from verified JWT, (3) user looked up in the appropriate database to get role + scope, (4) authorization check runs against the server-derived role + scope, (5) database query filtered by server-derived identity (via TenantScopedDB for client endpoints, via direct employee_id filter for employee endpoints). No endpoint trusts client-sent IDs. No endpoint skips the authorization middleware.
Final Architecture Decisions
Architecture Decisions (Four-Brain Consensus)
1. Auth vs. Authz: Clerk handles authentication (identity). OA's Workers enforce authorization (access control) on every request, server-side, deriving identity from signed JWTs and never trusting client-sent IDs.
2. Data stores: Two separate D1 databases (oa-employee-db for HIGH sensitivity PII/payroll, oa-client-db for MEDIUM sensitivity business metrics). Admin Worker bridges both. Client and Employee Workers each bind to only their own.
3. Client tenant isolation: ONE shared client database with TenantScopedDB middleware. Every table has a company_id column. Every query is automatically scoped. Handlers never access the raw DB binding. Per-client databases were rejected as over-engineering that does not scale to 5,000 clients.
4. Auth provider: Clerk (managed). Auth is the one domain where specialist beats generalist. Free for the first 10K MAU. SOC 2 compliant. Handles the subtle edge cases (token rotation, brute-force, session fixation) that hand-built auth misses.
5. VA cross-reference: Minimal VirtualAssistants table in client DB with display-only fields. No database merging. Opaque employee reference ID. Tenant-scoped like every other table.
6. Admin panel: An orchestrator Worker with dual DB bindings, NOT a third database. Routes to the correct database based on the request path. Admin users authenticated via the employee database (since admins are OA employees). All writes audit-logged.
7. Scale principle: Architecture designed for 5,000 clients, not 150. Operational complexity is O(1), not O(N). Cross-client analytics, AI training, and portfolio reporting are first-class capabilities, not afterthoughts.
8. Every isolation claim has a test: 8 concrete test scenarios with GIVEN/WHEN/THEN + mechanistic code explanations. Tenant isolation, cross-portal isolation, role-based access, and forged JWT defense are all tested.
9. Security checklist: 6-point reusable protocol. Apply to every new endpoint and feature before it ships.