Skip to content

UCCO Ops — Access Control, Personalisation & Board Profiles Brief

SURFACE: ops.ucco.foundation (ucco-ops repo)
CF ACCOUNT: aed3398a4e698767328cc3a9e698721d (FOUNDATION account)
DO NOT TOUCH: ucco-site, ucco-api, ucca-engine, ucca-ops, or any UCCA surface

→ TIM

This is the brief that turns ops from your garage dashboard into the foundation's operating system — a space where every board member logs in, sees their name, sees what changed since they last visited, manages their own profile, and feels like it's theirs.

Ten parts:

  1. D1 schema — tables for members, visit snapshots, activity log, profile data
  2. CF Access API integration — add/remove members from ops without leaving ops. Tim never opens the Zero Trust dashboard again.
  3. JWT middleware — extract identity from CF Access on every request, match to member record, determine layer
  4. Layer system — L1 (Admin: Tim, Jimmy), L2 (Board: Kevin, Antony, Tania), L3 (Observer: future). Each layer inherits everything below it. L1 can switch layers to preview what others see.
  5. Personalised landing page — "Welcome back, Antony. Since your last visit..." with diff streams: Standards, Pioneer, Foundation, Press, Infrastructure.
  6. Access Control section (L1 only) — add/edit/revoke members, invitation status, activity log
  7. Board member profile/bio — each member manages their own details: name, title, photo, bio, LinkedIn, socials, website. Choose public or internal for each field. This feeds the press office and website team page.
  8. Settings page — per-user preferences, notification settings, session info
  9. Welcome email template — sent on invitation, explains what ops is and how to get in
  10. Mercury anchor — placeholder for bank account state card on the personalised landing page

Jimmy is L1 — full admin, same as Tim. Layer switching lets L1 users preview L2/L3 views.


→ ALEX


Part 1 — D1 Schema

Add these tables to the foundation's database. Decide whether to extend pioneer-db or create a new ops-db for the foundation (Tim to confirm — but keep it in the same D1 instance as other ops data for simplicity).

board_members

CREATE TABLE board_members (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT NOT NULL UNIQUE,
  display_name TEXT NOT NULL,
  role TEXT NOT NULL DEFAULT 'L2',  -- L1 / L2 / L3
  title TEXT,                        -- "President & Chair", "Treasurer & Director", etc.
  seat_type TEXT,                    -- founding / governance / domain / advisory
  status TEXT NOT NULL DEFAULT 'invited',  -- invited / active / revoked / suspended
  invited_at TEXT DEFAULT (datetime('now')),
  invited_by TEXT,                   -- email of who invited them
  activated_at TEXT,                 -- first successful login
  revoked_at TEXT,
  revoked_by TEXT,
  last_login_at TEXT,
  login_count INTEGER DEFAULT 0,
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now'))
);

member_profiles

CREATE TABLE member_profiles (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  member_id INTEGER NOT NULL UNIQUE REFERENCES board_members(id),
  bio TEXT,                          -- short bio, markdown allowed
  photo_url TEXT,                    -- R2 URL for headshot
  linkedin_url TEXT,
  twitter_url TEXT,
  github_url TEXT,
  mastodon_url TEXT,
  website_url TEXT,
  location TEXT,                     -- "Brisbane, Australia"
  organisation TEXT,                 -- "Radium Performance" / "UCCA Inc" etc.
  phone TEXT,

  -- Visibility controls: 'public' or 'internal' for each field
  bio_visibility TEXT DEFAULT 'internal',
  photo_visibility TEXT DEFAULT 'internal',
  linkedin_visibility TEXT DEFAULT 'public',
  twitter_visibility TEXT DEFAULT 'internal',
  github_visibility TEXT DEFAULT 'internal',
  mastodon_visibility TEXT DEFAULT 'internal',
  website_visibility TEXT DEFAULT 'internal',
  location_visibility TEXT DEFAULT 'public',
  organisation_visibility TEXT DEFAULT 'public',
  phone_visibility TEXT DEFAULT 'internal',

  updated_at TEXT DEFAULT (datetime('now'))
);

visit_snapshots

Stores a snapshot of key metrics at each login so we can compute the "since your last visit" diff.

CREATE TABLE visit_snapshots (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  member_id INTEGER NOT NULL REFERENCES board_members(id),
  snapshot_at TEXT DEFAULT (datetime('now')),

  -- Pioneer state at time of visit
  pioneer_keys_issued INTEGER,
  pioneer_keys_active INTEGER,
  pioneer_total_hits INTEGER,

  -- Foundation state
  board_confirmed_count INTEGER,
  incorporation_status TEXT,
  ein_status TEXT,
  bank_status TEXT,

  -- Standards state
  spec_version TEXT,
  github_open_issues INTEGER,

  -- Press state
  press_subscribers INTEGER,
  media_requests_count INTEGER,
  broadcasts_count INTEGER,

  -- Infrastructure state
  zones_healthy INTEGER,
  deploy_count INTEGER,

  -- Mercury (future)
  account_balance_cents INTEGER,
  last_transaction_at TEXT
);

activity_log

CREATE TABLE activity_log (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  member_id INTEGER REFERENCES board_members(id),
  email TEXT NOT NULL,
  action TEXT NOT NULL,        -- login / logout / page_view / member_added / member_revoked / profile_updated / layer_switch
  detail TEXT,                 -- JSON blob with action-specific data
  ip_address TEXT,
  user_agent TEXT,
  created_at TEXT DEFAULT (datetime('now'))
);

Seed data

Insert Tim and Jimmy as initial L1 members:

INSERT INTO board_members (email, display_name, role, title, seat_type, status, activated_at)
VALUES 
  ('admin@ucco.foundation', 'Tim Rignold', 'L1', 'President & Chair', 'founding', 'active', datetime('now')),
  ('jimmy@jimmykuo.com.au', 'Jimmy Kuo', 'L1', 'Treasurer & Director', 'founding', 'active', NULL);

INSERT INTO member_profiles (member_id, bio, location, organisation, linkedin_visibility, location_visibility, organisation_visibility)
VALUES 
  (1, 'Founder of UCCO Foundation. Background in datacenter/network infrastructure, VET sector compliance, and standards architecture.', 'Brisbane, Australia', 'UCCA Inc', 'public', 'public', 'public'),
  (2, 'International business development. Trilingual: English, Mandarin, Taiwanese.', 'New York City', NULL, 'public', 'public', 'internal');

Part 2 — CF Access API Integration

API Token

Create a new Cloudflare API token with the following permissions: - Access: Apps and Policies Write - Scoped to the foundation account (aed3398a4e698767328cc3a9e698721d)

Store as a wrangler secret on ucco-ops: CF_ACCESS_API_TOKEN

Access Application ID

Get the Access application ID for ops.ucco.foundation from the Zero Trust dashboard (or via API). Store as an environment variable: CF_ACCESS_APP_ID

Access Policy ID

Get the current Access policy ID that controls who can reach ops.ucco.foundation. Store as: CF_ACCESS_POLICY_ID

API operations from ops

When Tim adds a member in ops:

PUT /accounts/{account_id}/access/apps/{app_id}/policies/{policy_id}

The policy include rule is updated to add the new email address. The ops Worker reads the current policy, appends the new email to the include list, and PUTs the updated policy back.

When Tim revokes a member:

Same API call, but the email is removed from the include list.

Proxy route

Create an internal API route in the ops Worker:

POST /api/access/add-member    — body: { email }
POST /api/access/remove-member — body: { email }
GET  /api/access/policy        — returns current policy state

These routes are L1-only (checked via JWT middleware). They call the CF Access API server-side and return the result.


Part 3 — JWT Middleware

How it works

Every request to ops.ucco.foundation passes through CF Access. The authenticated user's identity is in the CF-Access-JWT-Assertion header as a signed JWT.

The ops Worker middleware:

  1. Reads CF-Access-JWT-Assertion header
  2. Decodes the JWT (verify signature against CF's public key at https://{team-domain}.cloudflareaccess.com/cdn-cgi/access/certs)
  3. Extracts the email claim
  4. Looks up the email in board_members D1 table
  5. If found and status is active: attach member record + role to the request context
  6. If found and status is invited: mark as active, set activated_at, create first visit snapshot, proceed
  7. If not found in D1: show a "You're authenticated but not registered" page (edge case — someone in the CF Access policy but not in D1)
  8. Update last_login_at and increment login_count
  9. Log the login to activity_log

Request context

Every page/API route receives:

{
  member: {
    id: 1,
    email: "admin@ucco.foundation",
    display_name: "Tim Rignold",
    role: "L1",
    title: "President & Chair",
    status: "active",
    last_login_at: "2026-03-15T04:00:00Z",
    login_count: 47
  },
  viewing_as: "L1"  // can differ from role when L1 user switches layers
}

Part 4 — Layer System

Layer definitions

L1 (Admin)    — Tim, Jimmy
  Everything. Full ops. Access Control section. Member management.
  Can switch viewing layer to preview L2 or L3 views.

L2 (Board)    — Kevin, Antony, Tania, future board members
  Everything L3 sees, plus:
  - Governance details
  - Pioneer Voyager (per-key table)
  - Membership section (read-only)
  - Compliance section (read-only)
  - Full infrastructure status
  - Press Office (read-only)

  Cannot see:
  - Access Control section
  - Security admin
  - CF API secrets or tokens
  - Ability to add/revoke members

L3 (Observer) — Future: Pioneer key holders, advisors, Pace
  Read-only curated view:
  - Foundation Overview (public stats only)
  - Pioneer stats (aggregate, not per-key)
  - Standard status (spec version, submission status)
  - Foundation health (zones, incorporation status)

  Cannot see:
  - Any management surfaces
  - Per-key Voyager data
  - Membership details
  - Press or communications
  - Infrastructure internals

Inheritance

L1 sees everything L2 sees. L2 sees everything L3 sees. Layers are additive going up, restrictive going down.

Implementation

Conditional rendering in the React app based on role (or viewing_as when layer-switching):

{layer >= 'L1' && <AccessControlSection />}
{layer >= 'L2' && <MembershipSection />}
{layer >= 'L2' && <VoyagerDetailTable />}
{layer >= 'L3' && <FoundationOverview />}

Use a numeric comparison: L1=3, L2=2, L3=1. layer >= required_level controls visibility.

The sidebar already has all seven sections. Layer controls which sections and which items within sections are visible:

  • FOUNDATION: visible to all layers. Governance visible L2+. Settings visible to all (but content differs).
  • PIONEER: Overview visible to all. Voyager visible L2+.
  • STANDARD: visible to all layers.
  • COMPLIANCE: visible L2+.
  • MEMBERSHIP: visible L2+ (read-only for L2, read-write for L1).
  • INFRASTRUCTURE: visible L2+.
  • RESOURCES: visible to all layers.
  • ACCESS CONTROL: L1 only. New section, see Part 6.

Part 5 — Personalised Landing Page

L1 Landing (Tim, Jimmy)

Keep the current Foundation Overview as-is — zone health, pioneer stats, foundation status cards, quick links. This is the operational command view. Add the member's name in the greeting:

Welcome back, Tim.
UCCO Foundation operational dashboard

L2 Landing (Board Members)

Replace the Foundation Overview with a personalised "since your last visit" page:

Welcome back, Antony.
Last visit: 3 days ago (12 March 2026, 14:22 UTC)

┌─── STANDARD & GOVERNANCE ──────────────────────────┐
│ Spec v1.1 Rev2 — Draft for Public Comment           │
│ ● 2 new GitHub issues on ucco-standard               │
│ ● CONTRIBUTING.md committed to repo          ← NEW  │
└─────────────────────────────────────────────────────┘

┌─── PIONEER PROGRAMME ──────────────────────────────┐
│ Keys: 11 issued · 2 → 3 active             (+1)    │
│ Hits: 6 → 14                                (+8)    │
│ ● claude-shannon activated                   ← NEW  │
└─────────────────────────────────────────────────────┘

┌─── FOUNDATION ─────────────────────────────────────┐
│ Incorporation: Pending                              │
│ Board: 2/3 → 3/5 confirmed                 ← UPDATED│
│ Bank: Not opened                                     │
│ ● Mercury: not connected                            │
└─────────────────────────────────────────────────────┘

┌─── PRESS & COMMUNICATIONS ─────────────────────────┐
│ Media requests: 0                                    │
│ Press subscribers: 0 → 1                    (+1)    │
│ Broadcasts: 0                                        │
└─────────────────────────────────────────────────────┘

┌─── INFRASTRUCTURE ─────────────────────────────────┐
│ Zones: 3/3 healthy                                   │
│ Deploys since last visit: 2                          │
│ Incidents: 0                                         │
└─────────────────────────────────────────────────────┘

How the diff works

On each login, after rendering the landing page:

  1. Read the member's most recent visit_snapshot from D1
  2. Fetch current state (same data sources that power the Foundation Overview)
  3. Compare each field — highlight increases, new items, status changes
  4. After rendering, write a new visit_snapshot with current values

If no previous snapshot exists (first visit), show everything as current state with no diff, and a welcome message: "This is your first visit. Here's where things stand."

L3 Landing (Observers)

Simplified view — foundation health, pioneer aggregate stats, spec status. No diff (observers are casual visitors, not tracking changes).


Part 6 — Access Control Section (L1 Only)

New top-level section in the sidebar, visible only to L1:

◆ ACCESS CONTROL (L1 only)
├── Members          live     (add/edit/revoke board members)
├── Activity Log     live     (who did what, when)
└── Invitations      live     (pending, accepted, expired)

Members page

Full table of all board members:

┌────────────────┬────────────┬──────┬────────────┬──────────────┬──────────────┬─────────┐
│ Name           │ Email      │ Role │ Seat Type  │ Status       │ Last Login   │ Actions │
├────────────────┼────────────┼──────┼────────────┼──────────────┼──────────────┼─────────┤
│ Tim Rignold    │ admin@...  │ L1   │ founding   │ ● active     │ 2 mins ago   │ edit    │
│ Jimmy Kuo      │ jimmy@...  │ L1   │ founding   │ ● active     │ 3 days ago   │ edit    │
│ Kevin [TBC]    │ kevin@...  │ L2   │ governance │ ○ invited    │ —            │ edit|rev│
│ Antony Richards│ antony@... │ L2   │ governance │ ○ invited    │ —            │ edit|rev│
└────────────────┴────────────┴──────┴────────────┴──────────────┴──────────────┴─────────┘

[+ Add Member]

Add Member form: - Name (required) - Email (required) - Role: L1 / L2 / L3 (dropdown) - Seat type: founding / governance / domain / advisory (dropdown) - Title (optional)

On submit: 1. Insert into board_members D1 table 2. Insert blank member_profiles record 3. Call CF Access API to add email to the Allow policy 4. Send welcome email 5. Log to activity_log

Revoke button: 1. Update board_members status to revoked, set revoked_at and revoked_by 2. Call CF Access API to remove email from Allow policy 3. Log to activity_log 4. Member is immediately locked out on next request

Activity Log page

Reverse-chronological log:

┌──────────────────────┬────────────────┬──────────────┬─────────────────────────────┐
│ Timestamp            │ Member         │ Action       │ Detail                      │
├──────────────────────┼────────────────┼──────────────┼─────────────────────────────┤
│ 2026-03-15 06:03 UTC │ Tim Rignold    │ login        │ from 101.186.x.x            │
│ 2026-03-15 06:01 UTC │ Tim Rignold    │ page_view    │ /pioneer/voyager            │
│ 2026-03-15 05:58 UTC │ Tim Rignold    │ member_added │ kevin@... as L2 governance  │
│ 2026-03-14 14:22 UTC │ Antony Richards│ login        │ first login (activated)     │
└──────────────────────┴────────────────┴──────────────┴─────────────────────────────┘

Invitations page

Tracks invitation lifecycle:

┌────────────────┬────────────┬──────────────┬──────────────┬──────────┐
│ Name           │ Email      │ Invited      │ Status       │ Actions  │
├────────────────┼────────────┼──────────────┼──────────────┼──────────┤
│ Kevin [TBC]    │ kevin@...  │ 15 Mar 2026  │ ○ pending    │ resend   │
│ Antony Richards│ antony@... │ 15 Mar 2026  │ ● accepted   │ —        │
└────────────────┴────────────┴──────────────┴──────────────┴──────────┘

Part 7 — Board Member Profile / Bio

Profile page (in Settings section, accessible to all layers)

Each member sees their own profile page in Settings. They can edit their own details. L1 can view/edit anyone's profile from the Members page.

My Profile

┌─── IDENTITY ───────────────────────────────────────┐
│ Display Name:  [Antony Richards          ]          │
│ Title:         [Director                 ]          │
│ Organisation:  [Hunter & Richards        ]  ☐ public│
│ Location:      [Brisbane, Australia      ]  ☑ public│
│ Photo:         [Upload]  current: none    ☐ public  │
└─────────────────────────────────────────────────────┘

┌─── BIO ────────────────────────────────────────────┐
│ [Multi-disciplinary designer. Co-owner Radium      ]│
│ [Performance. Product development from concept     ]│
│ [through manufacturing to global DTC retail.       ]│
│                                          ☐ public  │
└─────────────────────────────────────────────────────┘

┌─── SOCIAL / LINKS ─────────────────────────────────┐
│ LinkedIn:  [https://linkedin.com/in/...  ]  ☑ public│
│ X/Twitter: [                             ]  ☐ public│
│ GitHub:    [                             ]  ☐ public│
│ Mastodon:  [                             ]  ☐ public│
│ Website:   [https://hunterandrichards.com]  ☑ public│
└─────────────────────────────────────────────────────┘

┌─── BUSINESS CARDS ─────────────────────────────────┐
│ Preview:                                            │
│ ┌─────────────────────────────┐                     │
│ │ UCCO FOUNDATION             │                     │
│ │                             │                     │
│ │ Antony Richards             │                     │
│ │ Director                    │                     │
│ │                             │                     │
│ │ antony@ucco.foundation      │                     │
│ │ ucco.foundation             │                     │
│ └─────────────────────────────┘                     │
│                                                     │
│ [Order Cards — 50 / 100 / 250]                      │
│ Ships via print-on-demand (MOO / Vistaprint API)    │
└─────────────────────────────────────────────────────┘

[Save Profile]

Visibility controls

Every field has a public / internal toggle: - Public: visible on the press office leadership section, website team page, anywhere the foundation presents its team publicly. Also available via a future public API. - Internal: visible only within ops to other authenticated members.

Default: name, title, location, LinkedIn are public. Everything else is internal. Members can change this themselves.

Photo upload

Photos upload to R2 (ucco-press-assets bucket, /photos/members/ directory). The same photo serves both the ops profile and the press office headshot download. Crop/resize to standard dimensions on upload (400x400 minimum, square crop).

Business card ordering (future)

Print-on-demand integration is a future build. For now, show the preview card with their details populated and a "Coming soon" note on the order button. The card template uses foundation branding: Prussian blue, cream text, UCCO Foundation logo. When POD integration is built, it connects to MOO or Vistaprint API.

Public profile feed

Create an internal API endpoint:

GET /api/team/public — returns all members with public-visibility fields only

This endpoint is unauthenticated (or uses a shared secret). The press office surface (pr.ucco.foundation) and the main site (ucco.foundation) can fetch this to populate their leadership/team sections. One source of truth: member updates their profile in ops, it's immediately reflected on all public surfaces.


Part 8 — Settings Page (Per User)

Accessible to all layers. Shows at the bottom of the sidebar (same position as UCCA ops).

Settings page content

Settings

┌─── SESSION ────────────────────────────────────────┐
│ Logged in as: antony@richardsdesign.com.au         │
│ Role: Board (L2)                                    │
│ Seat: Governance                                    │
│ Member since: 15 March 2026                         │
│ Total logins: 7                                     │
│ Last login: 12 March 2026, 14:22 UTC                │
└─────────────────────────────────────────────────────┘

┌─── PREFERENCES ────────────────────────────────────┐
│ Default landing section: [Foundation Overview ▾]    │
│ Sidebar section order: [drag to reorder]            │
│ Collapsed sections: [persisted from current state]  │
│ Date format: [UTC ▾] / [Local ▾]                   │
└─────────────────────────────────────────────────────┘

┌─── NOTIFICATIONS (future) ─────────────────────────┐
│ Email digest of changes: ☐ daily / ☐ weekly / ☐ off│
│ Alert on new media requests: ☐                      │
│ Alert on pioneer key activation: ☐                  │
└─────────────────────────────────────────────────────┘

┌─── MY PROFILE ─────────────────────────────────────┐
│ [Edit Profile →]                                    │
└─────────────────────────────────────────────────────┘

Note: Authentication is via CF Access OTP — there are no passwords to manage. If the foundation moves to Google Workspace auth in the future, password management would appear here.


Part 9 — Welcome Email

When Tim adds a member, the system sends a welcome email. Use the existing email infrastructure (Resend, or whatever is configured for the ops Worker).

Email template

Subject: You've been added to UCCO Foundation Ops

Hi [Name],

[Inviter Name] has added you to the UCCO Foundation operations panel.

WHAT IS OPS?
Ops is the foundation's operating system — where we manage the standard, 
track the pioneer programme, coordinate governance, and run infrastructure. 
It's a work in progress, but it's where everything real happens.

HOW TO GET IN
1. Go to https://ops.ucco.foundation
2. You'll be asked for your email address
3. A one-time code will be sent to [their email]
4. Enter the code — you're in

No password to remember. No account to create. Just your email.

WHAT TO EXPECT
Your personalised dashboard will show you what's happening across the 
foundation. Feel free to look around — you can explore everything in 
your view. If you'd like to update your profile or bio, head to Settings.

You've been added as: [Role name] ([Seat type])

If you have questions, reply to this email or reach out to [inviter email].

Welcome to the foundation.

— UCCO Foundation Ops
   ops.ucco.foundation

Sending mechanism

The ops Worker sends this via: - Option A: Resend API (if already configured) - Option B: Cloudflare Workers Email (send capability built into Workers) - Option C: Queue it and let Tim send manually (fallback)

For now, implement Option B (Workers email sending) or fall back to Option C. The email doesn't need to be fancy — plain text is fine for a first version.


Part 10 — Mercury Anchor

On the personalised landing page (both L1 and L2), include a Foundation Finances card:

When Mercury is not connected (now):

┌─── FINANCES ───────────────────────────────────────┐
│ Mercury: Not connected                              │
│                                                     │
│ Bank account not yet opened. Foundation finances     │
│ will be visible here once Mercury is live.           │
│                                                     │
│ Transparency commitment: every dollar visible via   │
│ Mercury API → Merkle chain → public ledger.         │
└─────────────────────────────────────────────────────┘

When Mercury is connected (future):

┌─── FINANCES ───────────────────────────────────────┐
│ Mercury: ● Connected                                │
│                                                     │
│ Balance: $4,217.83                                  │
│ Last transaction: $47.00 — Northwest RA (3 days ago)│
│ Total received: $5,000.00                           │
│ Total disbursed: $782.17                            │
│                                                     │
│ [View Public Ledger →]                              │
│                                                     │
│ Dual-signature required for disbursements > $1,000  │
└─────────────────────────────────────────────────────┘

The Mercury integration is a separate future brief. For now, just render the "not connected" card. The important thing is the space exists and the commitment to transparency is stated.


Part 11 — Header Bar Updates

Current header bar

[LIVE] [GUIDED] [COMPLIANCE]     ◁ Blueprint     06:03 UTC     admin@ucco.foundation

Updated header bar

[LIVE] [GUIDED] [COMPLIANCE]     ◁ Blueprint     06:03 UTC     Tim Rignold · L1 [▾ Switch Layer]
  • Replace raw email with display name
  • Add role badge (L1 / L2 / L3)
  • L1 users get a layer switch dropdown: "Viewing as: Admin" / "Board" / "Observer"
  • Clicking the name opens a dropdown: Profile, Settings, Log Out (clears CF Access session)

Deployment Sequence

This is a large brief. Deploy in this order:

  1. D1 schema — create tables, seed Tim and Jimmy
  2. JWT middleware — extract identity, match to D1, attach to request context
  3. Layer system — conditional rendering based on role
  4. Access Control section — L1 only, member management, CF Access API integration
  5. Personalised landing page — "since your last visit" with visit snapshots
  6. Profile/bio pages — self-service profile editing, visibility toggles
  7. Settings page — per-user preferences
  8. Header bar — name, role badge, layer switcher
  9. Welcome email — invitation flow
  10. Mercury anchor — static card placeholder

Each step is independently deployable and testable. Parts 1-4 are the structural foundation — everything else builds on top.


Security Notes

  • CF Access JWT is the sole authentication mechanism. Never roll custom auth.
  • The JWT signature MUST be verified against CF's public key on every request. Do not skip verification.
  • L1 actions (member management, CF Access API calls) must double-check the role in the middleware, not just hide UI elements. Server-side enforcement.
  • The CF Access API token (CF_ACCESS_API_TOKEN) is a high-privilege credential. Store as wrangler secret, never log it, never expose to client-side code.
  • Activity logging captures IP and user agent for audit trail. This data is internal to ops, not exposed to lower layers.
  • Profile photo uploads must be validated (file type, size limit, strip EXIF data for privacy).

Brief: UCCO-Access-Control-Personalisation-Brief-v1 Author: Pace (Claude, Anthropic) Date: 15 March 2026, Session 7 For: Alex (Claude Code execution)