Security Design — RBA Dev Team Portal v1.0.0

Build Docs · Gold

Security Design

Security model for both Firebase projects — access control rules, authentication architecture, admin claim setup, API key handling, and Duda-specific security considerations.

Security Overview

The RBA system uses two Firebase projects with different security models. RBAMGR (the case management system) has no authentication in Phase 1 — it relies on security-through-obscurity via hidden Duda pages and will add Firebase Auth in Phase 2. The Dev Team Portal is fully authenticated from day one using Firebase Email/Password Auth and a custom admin claim.

Project
Auth Model
Access Control
rba-mgr
None (Phase 1)
Hidden Duda pages + open DB rules during development. Firebase Auth + per-user rules added in Phase 2.
rba-dev-team-portal
Firebase Email/Password
Per-UID rules for volunteers. Custom admin claim ( auth.token.admin === true ) for John's dashboard.

RBAMGR — Phase 1 Security

rba-mgr

Phase 1 Database Rules

During Phase 1 development, the database uses open rules to allow read/write without authentication. These must be locked down before go-live.

// Phase 1 — Development only. Replace before production. { "rules": { ".read": true, ".write": true } }

Required Before Production

Open rules are acceptable during local development but must be replaced before any real case data is stored. See Phase 2 rules below. Add this to the Integration Phase checklist.

Phase 2 Database Rules (Target)

Once Firebase Auth is added in Phase 2, rules lock each arbitrator to their own node. The [arbitratorId] in the path matches the authenticated user's UID.

// Phase 2 — Production rules (to be applied when Auth is added) { "rules": { "arbitrators": { "$arbitratorId": { ".read": "auth != null && auth.uid === $arbitratorId", ".write": "auth != null && auth.uid === $arbitratorId" } }, "counters": { "$arbitratorId": { ".read": "auth != null && auth.uid === $arbitratorId", ".write": "auth != null && auth.uid === $arbitratorId" } }, "resources": { "$arbitratorId": { ".read": "auth != null && auth.uid === $arbitratorId", ".write": "auth != null && auth.uid === $arbitratorId" } }, ".read": false, ".write": false } }

Phase 1 Access Model

Without Firebase Auth, Phase 1 access control relies on:

Phase 1 Security Layers
  • Hidden Duda pages: All RBAMGR pages are hidden from the Duda navigation — they don't appear in any site menu or sitemap. Access is by direct URL only.
  • No public link exposure: The arbitrator bookmarks each page. URLs are not shared publicly and not linked from any public-facing page.
  • Arbitrator ID in code: Each component hardcodes the arbitrator's ID string. Without knowing both the URL and the arbitrator ID, data cannot be targeted.
  • Single-user system: Phase 1 is built for one arbitrator. No login, no multi-user surface, no exposed credential entry points.

Phase 1 Limitation

Phase 1 security is adequate for a single-user private tool during development. It is not suitable for a multi-user or public-facing system. Firebase Auth and per-user rules are mandatory before Phase 2 launch.

Dev Team Portal — Full Auth Model

rba-dev-team-portal

Authentication Setup

The portal uses Firebase Authentication with Email/Password only. No Google, GitHub, or other OAuth providers. Every user account is created by John — volunteers receive an invite code and register with it.

Account Type
Created By
Notes
Admin (John)
Manually in Firebase console
Single account. Custom claim admin: true set via Node.js script.
Volunteers
Register with invite code
Invite code is single-use. admin/invites/[code]/used set true on first login.

Database Security Rules

{ "rules": { "volunteers": { "$volunteerId": { ".read": "auth != null && auth.uid === $volunteerId", ".write": "auth != null && auth.uid === $volunteerId" } }, "admin": { ".read": "auth != null && auth.token.admin === true", ".write": "auth != null && auth.token.admin === true" }, "assignments": { ".read": "auth != null", ".write": "auth != null && auth.token.admin === true" }, ".read": false, ".write": false } }

Rule-by-Rule Explanation

Path
Access
Why
volunteers/$volunteerId
Read + Write: own UID only
Each volunteer can only see and modify their own profile, tier, compensation, and submissions. No cross-volunteer visibility.
admin
Read + Write: admin claim only
Invite codes and admin-only data. Only John's account (with custom claim) can read or write this node.
assignments
Read: any auth user. Write: admin only
All logged-in volunteers can view assignments (needed to see their tasks). Only admin can create or close assignments.
/ (root)
Read + Write: denied
Default-deny catches any path not explicitly addressed above.

Admin Custom Claim

John's Firebase Auth account has a custom claim { admin: true } set via a Node.js script using the Firebase Admin SDK. This claim is checked on the database rules side ( auth.token.admin === true ) to grant access to the admin node and write access to assignments.

How the Claim Was Set

// Node.js script — run once, outside the browser // Uses Firebase Admin SDK (service account credentials required) const admin = require('firebase-admin'); admin.initializeApp({ credential: admin.credential.cert(serviceAccount) }); // Set custom claim on John's UID admin.auth().setCustomUserClaims('JOHNS_UID_HERE', { admin: true }) .then(() => console.log('Admin claim set.'));

Important: Token Refresh

Custom claims are embedded in the Firebase ID token, which is issued on login and cached for up to one hour. If the claim was just set, John must sign out and sign back in before the claim takes effect. The admin dashboard will show "access denied" until the token refreshes.

Checking the Claim in Client Code

// In portal pages that require admin access auth.onAuthStateChanged(function (user) { if (!user) { showView('view-unauth'); return; } user.getIdTokenResult().then(function (tokenResult) { if (tokenResult.claims.admin !== true) { showView('view-unauth'); return; } // Admin confirmed — show dashboard showView('view-dashboard'); loadVolunteers(); }); });

API Key Handling

Anthropic API Key (RBAMGR)

The Claude API key used for AI image intake is entered by the arbitrator in the Settings page and stored in localStorage — never in Firebase. This keeps it off the network and out of the database entirely.

Storage Method
Rationale
localStorage (browser only)
Never transmitted to Firebase. Survives page refresh. Lost if browser data is cleared — user re-enters it in Settings.
Masked display after entry
Settings page shows the key as sk-ant-••••••••[last4] after save. The full value is never re-rendered in the UI.
Not synced across devices
Known Phase 1 limitation. Multi-device API key sync is a Phase 2 enhancement (stored encrypted in Firebase).

Phase 1 Limitation

localStorage is device-specific. If the arbitrator uses RBAMGR on a different browser or device, they must re-enter the API key in Settings. This is documented behavior, not a bug.

Brevo API Key (Dev Team Portal)

The Brevo API key for the Dev Team Portal is hardcoded in the portal admin pages. This is acceptable because:

  • The admin pages are hidden from navigation and accessible only to John
  • The Brevo key only permits sending email — it cannot read existing contacts or perform destructive operations
  • Brevo allows IP allowlisting and rate limiting as an additional mitigation if desired

Firebase Configuration Keys

Both Firebase projects expose their configuration objects (including API keys) in client-side JavaScript. This is expected and safe — Firebase client API keys are not secret credentials. They identify the Firebase project to the Firebase SDK and are always visible in browser source.

Security for Firebase is enforced entirely by the database security rules (which run server-side) and by Firebase Auth token validation. The API key alone cannot bypass these rules.

Firebase API Key vs. Secret Key

Firebase client config keys (the apiKey in firebaseConfig ) are project identifiers, not authentication secrets. They restrict usage via Firebase's allowed domains, but the real protection is in security rules. The Anthropic API key, by contrast, is a secret — hence its storage in localStorage, not in code.

Duda-Specific Security

Duda's platform introduces a few security considerations that differ from a standard web host.

Duda Security Model
  • Hidden pages: All RBAMGR and portal pages are set to "Hidden from Navigation" in Duda's page manager. They do not appear in sitemaps, navigation menus, or Duda's search index. Access is URL-only.
  • No server-side code: Duda widgets run entirely client-side. All Firebase interactions happen in the browser. There is no server that could log or intercept data.
  • Duda password protection: Duda's Business plan supports page-level password protection as an additional optional layer. This is not currently configured but can be added to admin pages.
  • Source code visibility: Because Duda renders HTML in the browser, all JavaScript (including Firebase config and logic) is visible in browser DevTools. This is inherent to client-side web apps and mitigated by robust security rules.
  • Content Security Policy: Duda manages CSP headers at the platform level. Custom CSP configuration is not available on the Business plan. Firebase SDK and Google Fonts CDN domains are permitted by default.

Pre-Production Security Checklist

Run this checklist before any real case data enters RBAMGR, and before the Dev Team Portal goes live with real volunteers.

  • RBAMGR: Replace open ".read": true, ".write": true rules with per-arbitrator UID rules
  • RBAMGR: Confirm Firebase Auth is enabled and arbitrator UID is set in all component files
  • Portal: Verify admin custom claim is active — sign out and back in, then confirm admin dashboard is accessible
  • Portal: Test that a volunteer account cannot read another volunteer's node (attempt with browser DevTools)
  • Portal: Test that a volunteer account cannot write to admin/ or create assignments
  • Both: Confirm all invite codes are single-use and marked used: true after first login
  • Both: Verify browser console shows zero Firebase permission errors on normal workflows
  • Both: Confirm all pages are set to hidden in Duda's navigation settings