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:
- D1 schema — tables for members, visit snapshots, activity log, profile data
- CF Access API integration — add/remove members from ops without leaving ops. Tim never opens the Zero Trust dashboard again.
- JWT middleware — extract identity from CF Access on every request, match to member record, determine layer
- 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.
- Personalised landing page — "Welcome back, Antony. Since your last visit..." with diff streams: Standards, Pioneer, Foundation, Press, Infrastructure.
- Access Control section (L1 only) — add/edit/revoke members, invitation status, activity log
- 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.
- Settings page — per-user preferences, notification settings, session info
- Welcome email template — sent on invitation, explains what ops is and how to get in
- 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:
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:
- Reads
CF-Access-JWT-Assertionheader - Decodes the JWT (verify signature against CF's public key at
https://{team-domain}.cloudflareaccess.com/cdn-cgi/access/certs) - Extracts the
emailclaim - Looks up the email in
board_membersD1 table - If found and status is
active: attach member record + role to the request context - If found and status is
invited: mark asactive, setactivated_at, create first visit snapshot, proceed - 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)
- Update
last_login_atand incrementlogin_count - 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.
Sidebar changes¶
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:
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:
- Read the member's most recent
visit_snapshotfrom D1 - Fetch current state (same data sources that power the Foundation Overview)
- Compare each field — highlight increases, new items, status changes
- After rendering, write a new
visit_snapshotwith 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:
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¶
Updated header bar¶
- 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:
- D1 schema — create tables, seed Tim and Jimmy
- JWT middleware — extract identity, match to D1, attach to request context
- Layer system — conditional rendering based on role
- Access Control section — L1 only, member management, CF Access API integration
- Personalised landing page — "since your last visit" with visit snapshots
- Profile/bio pages — self-service profile editing, visibility toggles
- Settings page — per-user preferences
- Header bar — name, role badge, layer switcher
- Welcome email — invitation flow
- 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)