server.outsourceaccess.com -- Database design, RBAC, data pipeline, auth flow, and implementation roadmap
The OA Portal (server.outsourceaccess.com) serves two fundamentally different audiences through a single unified platform. Every user authenticates via Google OAuth. The system determines their role, resolves their permissions via the RBAC hierarchy, and renders a personalized dashboard with only the data and tools they are authorized to see.
| Dimension | Current State (Sep 2026) | Target State (Phase 2) |
|---|---|---|
| Authentication | Google OAuth on admin portal; employee/client dashboards have no auth gate | Every page behind Google OAuth with session cookie + RBAC middleware |
| Data Source | Static HTML with hardcoded data; manually updated per deploy | D1-backed with Cloudflare Worker sync jobs pulling live data from HubSpot, Time Doctor, Zoho, GWS |
| Employee Dashboards | 15 static HTML pages deployed (3 OM, 4 AM, 8 TL) | Single dynamic template rendering personalized data per user from D1 |
| Client Dashboards | 19 static HTML pages, no login required, URL-guessable | Auth-gated client portal with per-client data isolation from D1 |
| RBAC | Basic: admin vs non-admin check on admin portal only | Full 7-tier hierarchy with granular permissions, data scoping, audit trail |
| URL Structure | Flat: /dashboard/oa-{name}/ per person | Semantic: /dashboard/{role}/{slug}/ with role-based routing |
| Phase | Focus | Outcome | Status |
|---|---|---|---|
| Phase 1A | Auth-gate every page | Zero unauthenticated access to any dashboard | In Progress |
| Phase 1B | URL migration + RBAC | Semantic URLs, full role hierarchy, permission matrix | Planned |
| Phase 1C | Data pipeline Workers | Live data sync from HubSpot, Time Doctor, Zoho Recruit | Not Started |
| Phase 1D | Dynamic dashboards | Replace static HTML with D1-backed rendering engine | Not Started |
| Phase 2 | Client portal | Authenticated client dashboards with engagement visibility | Planned |
| Phase 3 | Market intelligence + AI | Per-client AI dashboards, industry intelligence, competitor monitoring | Planned |
| Phase 4 | External tool replacement | Move Time Doctor, Zoho, HubSpot functions into native OA platform | Future |
Complete path taxonomy for the OA portal. Green dots indicate paths that exist today. Yellow dots are partially implemented. Red dots are not yet built. Blue dots are planned for a future phase.
Migration strategy: serve both old and new URLs during transition via Cloudflare redirect rules. Old URLs return 301 to new canonical path. No broken bookmarks.
The RBAC system uses a 7-tier hierarchy with adjacency-list storage and recursive CTE resolution. Each role inherits the view permissions of all roles below it. Data scoping is additive going up the chain: a TL sees their own VAs' data, an OM sees all TLs' data under them, and so on.
| Permission Category | VA | TL | OM | AM | Vision | Admin | Owner | Client |
|---|---|---|---|---|---|---|---|---|
| Own profile / HR info | ||||||||
| Own time tracking data | ||||||||
| Direct reports' time data | ||||||||
| Performance reviews (write) | ||||||||
| Client engagement data | ||||||||
| Compensation data | ||||||||
| Company-wide financials | ||||||||
| Admin portal access | ||||||||
| User management (CRUD) | ||||||||
| Audit log access |
Full access Scoped / partial No access
| Role | Data Scope | Resolution Method |
|---|---|---|
| VA | Own records only (time, HR, benefits, tasks) | WHERE user_id = :current_user |
| Team Leader | Own records + all VAs in their team | WHERE user_id IN (SELECT employee_user_id FROM employee_reporting WHERE manager_user_id = :current_user) |
| Ops Manager | Own records + all TLs and VAs in their reporting chain | Recursive CTE traversing employee_reporting from OM down through TLs to VAs |
| Account Manager | All clients assigned to them + engagement data for those clients | WHERE client_id IN (SELECT client_id FROM client_assignments WHERE user_id = :current_user) |
| Vision Team | Company-wide data (all employees, all clients, financials) | No scope filter (full SELECT) |
| Client | Own engagement data only (their VAs, their hours, their advisory docs) | WHERE client_id = :current_client_id (isolated tenant) |
| Category | Examples | Min. Role Required | Classification |
|---|---|---|---|
| Compensation | Salary, bonuses, payroll data, Airwallex wire amounts | exec-only | Restricted |
| HR Records | Benefits, leave history, disciplinary notes, health insurance | self + manager | Confidential |
| Client Financials | Billing history, MRR, contract terms, pricing | manager + client | Confidential |
| Performance Reviews | Evaluation scores, manager notes, improvement plans | manager-only | Confidential |
| Engagement Metrics | VA hours, activity rates, task completion | employee + client | Internal |
| Company Directory | Names, roles, team assignments, photos | all employees | Internal |
The target architecture uses Cloudflare Workers as scheduled sync jobs that pull data from external APIs and write it to D1 (SQLite). The dashboard frontend reads from D1 via Cloudflare Functions API endpoints. Every API response includes data freshness metadata so the frontend can display staleness indicators when data is older than the expected sync interval.
| Worker | External Source | Data Synced | Interval | D1 Tables Written | Status |
|---|---|---|---|---|---|
| hubspot-sync | HubSpot CRM API | Contacts, companies, deals, tickets, CSAT surveys, engagements | 30 min | hs_contacts, hs_companies, hs_deals, hs_tickets, hs_csat | Not Started |
| timedoctor-sync | Time Doctor API v2 | Work logs, activity rates, screenshots, projects, tasks | 60 min | td_worklogs, td_activity, td_projects | Not Started |
| zoho-sync | Zoho Recruit API | Candidates, job openings, interviews, applications | 2 hours | zr_candidates, zr_openings, zr_applications | Not Started |
| gdrive-sync | Google Drive API | Playbook file links, file counts per client folder, last modified dates | 4 hours | gd_files, gd_playbooks | Not Started |
Every API response from the Functions layer includes a freshness envelope so the frontend can show staleness warnings. If data is older than 2x the expected sync interval, a yellow "stale" indicator appears. If older than 4x, a red "very stale" indicator appears.
The portal uses Google OAuth 2.0 (GCP project on brad@outsourceaccess.com) to authenticate all users. After successful OAuth, a session is created in D1 and a HttpOnly cookie is set. Every subsequent request passes through _middleware.js which validates the session cookie against D1 before allowing access.
| Property | Value | Rationale |
|---|---|---|
| Cookie Name | oa_session | Prefixed with oa_ to avoid collisions |
| HttpOnly | Yes | Prevents JavaScript access (XSS protection) |
| Secure | Yes | Only transmitted over HTTPS |
| SameSite | Lax | Prevents CSRF while allowing top-level navigations |
| Expiration | 24 hours (sliding window) | Active users stay logged in; inactive sessions expire automatically |
| Storage | D1 sessions table | Server-side validation on every request; sessions can be revoked instantly |
| Domain | server.outsourceaccess.com | Scoped to portal domain only |
The _middleware.js file runs on every request to Cloudflare Functions. It intercepts the request, validates the session cookie against D1, resolves the user's role and permissions, and either allows the request to proceed or redirects to /login/.
| Step | Check | Pass | Fail |
|---|---|---|---|
| 1 | Is path in public allowlist? (/login/, /login/callback, /api/auth/*, static assets) | Allow request to proceed | Continue to step 2 |
| 2 | Does request have oa_session cookie? | Continue to step 3 | 302 redirect to /login/?redirect={original_path} |
| 3 | Is session token valid in D1 sessions table? | Continue to step 4 | Clear cookie, 302 to /login/ |
| 4 | Is session expired? (> 24h since last activity) | Extend session, continue to step 5 | Delete session from D1, clear cookie, 302 to /login/ |
| 5 | Does user's role have permission for requested path? | Set user context on request, allow through | 403 Forbidden page |
The _routes.json file controls which paths are routed through Cloudflare Functions (and therefore through middleware). Static assets are excluded to avoid unnecessary function invocations.
| Path / Area | Current Auth State | Risk Level | Fix Phase |
|---|---|---|---|
| /admin/* | Protected -- Google OAuth + admin role check | Low | Done |
| /dashboard/oa-*/ (15 employee dashboards) | Unprotected -- Public HTML, URL-guessable | High | Phase 1A |
| /client/*/ (19 client dashboards) | Unprotected -- Public HTML, URL-guessable | High | Phase 1A |
| /api/data/* endpoints | Partial -- Some auth checks, inconsistent | Medium | Phase 1A |
| Static assets (CSS, JS, images) | Excluded -- Intentionally public (no sensitive data) | Low | N/A |
Summary: 34 dashboard pages (15 employee + 19 client) are currently accessible without authentication. This is the highest-priority security gap and is addressed in Phase 1A.
| Component | Status | Count | Auth State | Data Source | Priority | Notes |
|---|---|---|---|---|---|---|
| Admin Portal | Live | 1 | Protected | D1 + Static | Maintenance | Google OAuth, admin role gate, sidebar tabs |
| OM Dashboards | Live | 3 | None | Static HTML | Critical | Melchor, Treve, Jerome. Hardcoded data, no auth gate. |
| AM Dashboards | Live | 4 | None | Static HTML | Critical | Paola, Beia, Emily, Mia. Hardcoded data, no auth gate. |
| TL Dashboards | Live | 8 | None | Static HTML | Critical | 8 TLs. Hardcoded data, no auth gate. |
| Vision Team Dashboards | Not Started | 0 / 5 | -- | -- | High | Mary, Cleo, Leonard, Alva, Stephen Bill. Need executive-level views. |
| Client Dashboards | Live | 19 | None | Static HTML | Critical | URL-guessable. Contains client engagement data. |
| D1 Database (users, sessions, org) | Partial | 1 | Protected | D1 | High | Users + sessions tables exist. Org tree, permissions, dashboard config tables need creation. |
| Data Pipeline Workers | Not Started | 0 / 4 | -- | -- | High | HubSpot, Time Doctor, Zoho, GDrive sync workers not built. |
| RBAC System | Basic | -- | Partial | D1 | High | Currently only admin vs. non-admin. Full 7-tier hierarchy needed. |
| Login Page | Live | 1 | -- | OAuth | Maintenance | Google OAuth working. Redirect-after-login implemented. |
| _middleware.js | Partial | 1 | -- | -- | Critical | Runs on /admin/*. Needs to extend to /dashboard/* and /client/*. |
| _routes.json | Partial | 1 | -- | -- | High | Needs /dashboard/* and /client/* added to include list. |
| Dynamic Dashboard Engine | Not Started | 0 | -- | -- | Planned | Template-based rendering from D1 config. Phase 1D. |
| Client Portal | Not Started | 0 | -- | -- | Planned | Auth-gated client login with isolated data views. Phase 2. |
| Audit Logging | Not Started | 0 | -- | -- | High | Table defined in schema. Middleware integration needed. |
The OA Portal target schema is organized into six logical groups. Every table lives in a single Cloudflare D1 database. Foreign keys enforce referential integrity. The schema supports the full RBAC hierarchy, dynamic dashboard rendering, cached data pipeline, and a complete audit trail.
Core identity, authentication, and permission resolution. The users table is the hub that every other group references.
Models the OA management hierarchy as an adjacency list so any employee's full chain of command can be resolved with a recursive CTE. Supports direct, dotted-line, and temporary reporting relationships.
Tracks client companies, their designated OA team members (AM, TL, VAs), and client-side contacts who can log in to the portal.
Drives the entire dynamic dashboard rendering engine. Dashboards are containers. Widgets are reusable data-display components. Access is controlled at both role and individual user level.
Manages external API connections, sync scheduling, cached results, and API credentials. The hybrid model caches 90% of data and proxies 10% in real-time for high-freshness needs.
| Data Source | Sync Interval | Data Types | Est. Records | Staleness Display |
|---|---|---|---|---|
| HubSpot | 30 min | Contacts, Companies, Deals, Tickets, Engagements | ~5,000 | Green <30m, Yellow 30-60m, Red >60m |
| Time Doctor | 60 min | Work sessions, Projects, Tasks, Screenshots | ~10,000/week | Green <1h, Yellow 1-2h, Red >2h |
| Zoho Recruit | 2 hr | Candidates, Job Openings, Applications | ~2,000 | Green <2h, Yellow 2-4h, Red >4h |
| Google Drive | 4 hr | Playbooks, SOPs, Advisory Docs metadata | ~500 | Green <4h, Yellow 4-8h, Red >8h |
| Google Chat | Real-time proxy | Space messages (on-demand, not cached) | N/A | Live indicator |
Immutable append-only logs for every sensitive data access, authentication event, and configuration change. Required for compliance and forensic analysis.
| Action | Resource Type | Trigger | Sensitive? |
|---|---|---|---|
| login | session | Every successful OAuth callback | No |
| view_compensation | employee_profile | Any access to compensation_tier field | Yes |
| view_client_billing | client | Access to monthly_rate or billing widgets | Yes |
| export_data | varies | Any CSV/Excel download from dashboard | Yes |
| modify_permissions | role_permissions | Any change to RBAC configuration | Yes |
| impersonate_user | session | Admin "View As..." feature | Yes |
| dashboard_access | dashboard | Every dashboard page load | No |
| # | Table | Group | Primary Key | Key Foreign Keys | Status |
|---|---|---|---|---|---|
| 1 | users | Auth | id (UUID) | -- | Exists |
| 2 | sessions | Auth | id (token) | user_id --> users | Planned |
| 3 | permissions | Auth | id | -- | Planned |
| 4 | role_permissions | Auth | (role, permission_id) | permission_id --> permissions | Planned |
| 5 | user_permission_overrides | Auth | id | user_id, permission_id | New |
| 6 | employee_reporting | Org | id | employee_user_id, manager_user_id --> users | Planned |
| 7 | employee_profiles | Org | user_id | user_id --> users, department_id --> departments | New |
| 8 | departments | Org | id | head_user_id --> users, parent_dept_id --> self | New |
| 9 | clients | Client | id | -- | Planned |
| 10 | client_assignments | Client | id | client_id --> clients, user_id --> users | Planned |
| 11 | client_contacts | Client | id | client_id --> clients, user_id --> users | New |
| 12 | dashboards | Dashboard | id | owner_user_id --> users | Planned |
| 13 | widgets | Dashboard | id | data_source_id --> data_sources | New |
| 14 | dashboard_widgets | Dashboard | id | dashboard_id, widget_id | New |
| 15 | dashboard_role_access | Dashboard | (dashboard_id, role) | dashboard_id --> dashboards | Planned |
| 16 | dashboard_user_access | Dashboard | (dashboard_id, user_id) | dashboard_id, user_id --> users | Planned |
| 17 | data_sources | Pipeline | id | -- | Planned |
| 18 | sync_log | Pipeline | id | data_source_id --> data_sources | Planned |
| 19 | cached_data | Pipeline | id | data_source_id --> data_sources | New |
| 20 | api_credentials | Pipeline | id | data_source_id --> data_sources | New |
| 21 | audit_log | Audit | id | user_id --> users | Planned |
| 22 | security_events | Audit | id | user_id --> users (nullable) | New |
Every page, API endpoint, and data request is authenticated and authorized. There are zero public pages (except /login/). Security is enforced at the Cloudflare Worker middleware layer before any request reaches D1 or external APIs.
| Header | Value | Purpose |
|---|---|---|
| Strict-Transport-Security | max-age=31536000; includeSubDomains | Force HTTPS for 1 year |
| X-Content-Type-Options | nosniff | Prevent MIME-type sniffing |
| X-Frame-Options | SAMEORIGIN | Block clickjacking (allow admin iframes) |
| Content-Security-Policy | default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' fonts.googleapis.com | Block XSS, restrict resource origins |
| Referrer-Policy | strict-origin-when-cross-origin | Limit referrer leakage |
| Permissions-Policy | camera=(), microphone=(), geolocation=() | Disable unused browser APIs |
| X-Request-ID | auto-generated UUID | Trace requests through audit log |
Certain data fields require elevated access and trigger audit logging on every read. These categories are enforced at the RBAC layer and cannot be bypassed by dashboard configuration.
| Data Category | Examples | Minimum Role | Audit Logged? | Encryption |
|---|---|---|---|---|
| Compensation | Salary tiers, rates, bonuses, pay history | Owner/Admin only | Every access | At rest (D1) |
| HR/Personal | Performance reviews, disciplinary records, PIPs | Manager + Admin | Every access | At rest (D1) |
| Client Financials | Monthly rates, billing history, contract terms | Owner/Admin + AM (own clients) | Every access | At rest (D1) |
| API Credentials | HubSpot keys, Time Doctor tokens, OAuth secrets | Owner only | Every access | AES-256-GCM |
| Export Payloads | CSV/Excel downloads of any dataset | Manager + Admin | Every export | N/A (transit only) |
| VA Performance | Hours, task completion, screenshots | TL (own team) + Manager + Admin | Standard | At rest (D1) |
| Client Health | CSAT scores, NPS, engagement metrics | AM (own clients) + Manager + Admin + Client | Standard | At rest (D1) |
oa_session)sessions table in D1. Checks: exists? not expired? user.status = 'active'??redirect=/dashboard/om/mary) so user lands back after login.users table and status is 'active'. Creates a new session row in D1. Sets HttpOnly, Secure, SameSite=Lax cookie. Logs the login in audit_log.oa_sessionEach phase builds on the previous one. Dependencies are called out explicitly. Estimated timelines assume Sterling execution speed (not human dev timelines).