Cross-Suite Authentication Architecture
Story: FAS-6.2 - Verify Cross-Suite SSO Last Updated: 2026-02-10
Overview
The Hello World Co-Op platform uses a cookie-based Single Sign-On (SSO) architecture that allows users to authenticate once and access all deployed suites without re-authenticating. Authentication is centralized through the oracle-bridge service, which proxies requests to the auth-service canister on the Internet Computer and manages HttpOnly cookies for secure token storage.
Architecture Diagram
Browser (*.helloworlddao.com)
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌──────────────┐ │
│ │dao-suite │ │think-tank │ │governance│ │otter-camp │ │
│ │ │ │ -suite │ │ -suite │ │ -suite │ │
│ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └──────┬──────┘ │
│ │ │ │ │ │
│ └──────────────┴──────┬───────┴───────────────┘ │
│ │ │
│ @hello-world-co-op/auth │
│ (shared auth package) │
│ │ │
│ credentials: 'include' │
│ + X-CSRF-Token header │
│ │
├─────────────────────────────┼───────────────────────────────────┤
│ Cookies (Domain: .helloworlddao.com) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ access_token (HttpOnly, Secure, Path=/) │ │
│ │ refresh_token (HttpOnly, Secure, Path=/api/auth) │ │
│ │ csrf_token (non-HttpOnly, Secure, Path=/) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────┼───────────────────────────────────┘
│ HTTPS
▼
┌───────────────────────┐
│ oracle-bridge │
│ (Node.js/Express) │
│ │
│ /api/auth/login │
│ /api/auth/wallet-login │
│ /api/auth/refresh │
│ /api/auth/logout │
│ /api/auth/session │
│ /api/auth/csrf-token │
└───────────┬────────────┘
│ IC Agent
▼
┌───────────────────────┐
│ auth-service │
│ (Rust canister) │
│ │
│ login_email_password │
│ refresh_tokens │
│ logout │
└────────────────────────┘Authentication Flow
1. Login Flow
User (browser) oracle-bridge auth-service (IC)
│ │ │
│ POST /api/auth/login │ │
│ {email, password, │ │
│ device_fingerprint} │ │
│ credentials: 'include' │ │
├────────────────────────────►│ │
│ │ login_email_password() │
│ ├──────────────────────────►│
│ │ │
│ │ {access_token, │
│ │ refresh_token, │
│ │ access_expires_at, │
│ │ user_id} │
│ │◄──────────────────────────┤
│ │ │
│ Set-Cookie: access_token │ │
│ Set-Cookie: refresh_token │ │
│ Set-Cookie: csrf_token │ │
│ Body: {user_id, │ │
│ access_expires_at} │ │
│◄────────────────────────────┤ │
│ │ │
│ Store session metadata │ │
│ in memory (expiry time) │ │1b. Wallet Login Flow (BL-058)
Wallet login supports Plug, NFID, Ledger, and Trezor. The flow varies by wallet type but converges at the oracle-bridge endpoint.
User (browser) oracle-bridge identity-gateway (IC)
│ │ │
│ WalletAdapter.connect() │ │
│ (browser-side SDK) │ │
│ │ │
│ POST /api/auth/wallet-login│ │
│ {wallet_type, │ │
│ delegation_chain, │ │
│ public_key?, │ │
│ device_fingerprint} │ │
│ credentials: 'include' │ │
├────────────────────────────►│ │
│ │ validate_session() │
│ ├──────────────────────────►│
│ │ │
│ │ {principal, verified} │
│ │◄──────────────────────────┤
│ │ │
│ │ get_user_by_ii_principal │
│ │ (or create_user_ii if │
│ │ new principal) │
│ │ │
│ Set-Cookie: access_token │ │
│ Set-Cookie: refresh_token │ │
│ Set-Cookie: csrf_token │ │
│ Body: {user_id, │ │
│ wallet_type} │ │
│◄────────────────────────────┤ │Wallet adapter types:
| Wallet | delegation_chain | public_key | Auth method |
|---|---|---|---|
| Plug | Delegation from extension | N/A | Extension API |
| NFID | Delegation from popup | DER-stripped bytes | NFID SDK |
| II | Delegation from II window | N/A | @dfinity/auth-client |
| Ledger | N/A | Raw secp256k1 (64 hex) | WebUSB + BIP-44 |
| Trezor | N/A | Raw secp256k1 (64 hex) | Trezor Connect |
Software wallets (Plug, NFID, II) provide delegation chains. Hardware wallets (Ledger, Trezor) provide raw public keys — the identity-gateway derives the principal from the key.
Frontend integration:
The @hello-world-co-op/auth package (v0.7.0+) provides:
WalletAdapterinterface — standard connect/disconnect/getLoginPayload contractregisterWalletAdapter()— register adapters at app initWalletSelectorReact component — renders wallet buttons with availability detectionuseWalletLogin()hook — handles the connect → POST → redirect flow
import {
registerWalletAdapter,
PlugWalletAdapter,
NFIDWalletAdapter,
LedgerWalletAdapter,
TrezorWalletAdapter,
WalletSelector,
} from '@hello-world-co-op/auth';
// Register at app init
registerWalletAdapter(new PlugWalletAdapter());
registerWalletAdapter(new NFIDWalletAdapter());
registerWalletAdapter(new LedgerWalletAdapter());
registerWalletAdapter(new TrezorWalletAdapter());
// Render on login page
<WalletSelector onSuccess={() => navigate('/dashboard')} />Wallet linking (membership canister):
The membership canister supports linking multiple wallet principals to one membership record via link_wallet(primary, linked). This allows a user to log in with any linked wallet and resolve to the same membership status. Linking is controller-only (not user-callable).
2. Session Check Flow (Cross-Suite Navigation)
When a user navigates from one suite to another (e.g., dao-suite to think-tank-suite):
User navigates to think-tank-suite
│
│ AuthProvider mounts
│ Calls checkSession()
│
│ GET /api/auth/session
│ credentials: 'include'
│ (browser auto-attaches access_token cookie)
├──────────────────────────────► oracle-bridge
│ │
│ │ Check access_token cookie
│ │ exists and is valid
│ │
│ {authenticated: true} │
│◄──────────────────────────────────┤
│
│ Update in-memory state:
│ isAuthenticated = true
│ Schedule token refresh3. Token Refresh Flow
5 min before access token expiry:
│
│ POST /api/auth/refresh
│ credentials: 'include'
│ X-CSRF-Token: <csrf_token from cookie>
│ {device_fingerprint, timezone}
├──────────────────────────────► oracle-bridge
│ │
│ │ Read refresh_token from cookie
│ │ Call auth-service.refresh_tokens()
│ │
│ Set-Cookie: access_token (new) │
│ Set-Cookie: refresh_token (new) │
│ Set-Cookie: csrf_token (rotated)│
│ {access_expires_at (new)} │
│◄──────────────────────────────────┤
│
│ Update in-memory expiry
│ Schedule next refresh4. Logout Flow
User clicks Logout on any suite:
│
│ POST /api/auth/logout
│ credentials: 'include'
│ X-CSRF-Token: <csrf_token>
├──────────────────────────────► oracle-bridge
│ │
│ │ Clear all auth cookies:
│ │ - access_token
│ │ - refresh_token
│ │ - csrf_token
│ │
│ Set-Cookie: (cleared) │
│◄──────────────────────────────────┤
│
│ Clear in-memory state
│ Clear localStorage remnants
│
│ When user visits other suite:
│ checkSession() → authenticated: false
│ → Redirect to /loginCookie Configuration
Cookie Types
| Cookie | HttpOnly | Secure | SameSite | Domain | Path | Max Age |
|---|---|---|---|---|---|---|
access_token | Yes | Yes (prod) | Configurable (default: strict) | .helloworlddao.com | / | 15 min (matches token TTL) |
refresh_token | Yes | Yes (prod) | Configurable (default: strict) | .helloworlddao.com | /api/auth | 7 days (matches token TTL) |
csrf_token | No | Yes (prod) | Configurable (default: strict) | .helloworlddao.com | / | 24 hours |
Cookie Attributes Explained
HttpOnly: Prevents JavaScript access to the cookie. Access and refresh tokens are HttpOnly to prevent XSS attacks from stealing tokens. The CSRF token is non-HttpOnly because the frontend must read it to include in request headers.
Secure: Ensures cookies are only sent over HTTPS. Always enabled in production. Can be disabled for local HTTP development.
SameSite: Controls when cookies are sent with cross-origin requests.
strict(default): Most secure; cookies only sent for same-site requests. May block cookies on direct cross-subdomain navigation from external links.lax: Cookies sent for top-level navigations and same-site requests. Recommended for cross-subdomain SSO because it allows cookies when users navigate between subdomains.none: Cookies sent for all requests. Requires Secure flag. Only use if suites are on different domains.
Domain: Set to
.helloworlddao.comto allow cookie sharing across all subdomains (e.g.,www.helloworlddao.com,staging.helloworlddao.com). When not set, cookies are scoped to the exact origin domain and cross-suite SSO will not work.Path: The refresh token is restricted to
/api/authto minimize exposure. Access and CSRF tokens are available on all paths.
Environment Variables
| Variable | Description | Default | Production Value |
|---|---|---|---|
AUTH_COOKIE_DOMAIN | Cookie domain for cross-subdomain sharing | null (current domain) | .helloworlddao.com |
AUTH_COOKIE_SECURE | Use HTTPS-only cookies | true in production, false in dev | true |
AUTH_COOKIE_SAME_SITE | SameSite attribute | strict | lax (recommended for SSO) |
Configuration Source
Cookie configuration is managed in oracle-bridge/src/config/cookies.ts:
export function getAuthCookieConfig(): AuthCookieConfig {
const domain = process.env.AUTH_COOKIE_DOMAIN || null;
const secure = process.env.AUTH_COOKIE_SECURE !== undefined
? process.env.AUTH_COOKIE_SECURE === 'true'
: isProduction;
// SameSite defaults to 'strict', supports 'lax' and 'none'
const sameSite = process.env.AUTH_COOKIE_SAME_SITE || 'strict';
return { domain, secure, sameSite };
}Production Warnings
The cookie config module logs warnings at startup if:
- Running in production without the
Secureflag AUTH_COOKIE_DOMAINis not set (cookies won't work cross-subdomain)SameSite=noneis set withoutSecureflag
Token Storage Strategy
In-Memory Session Metadata (Client-Side)
The @hello-world-co-op/auth package stores only metadata in memory:
sessionId: User ID for display purposesaccessExpiresAt: Timestamp for proactive refresh timing
No tokens are stored in JavaScript-accessible storage. The actual access token and refresh token are in HttpOnly cookies managed by the server.
On page refresh, in-memory state is lost. The AuthProvider calls checkSession() on mount to re-establish session state from cookies.
Token Manager (tokenManager.ts)
class TokenManager {
private sessionId: string | null = null;
private accessExpiresAt: number | null = null;
setSessionMetadata(sessionId: string, expiresAt: number): void { ... }
isSessionExpired(): boolean {
// 30-second buffer for network latency
return Date.now() >= this.accessExpiresAt - 30_000;
}
}Why Not localStorage?
Tokens stored in localStorage are vulnerable to XSS attacks. Any injected script can read localStorage and exfiltrate tokens. HttpOnly cookies are inaccessible to JavaScript, making them immune to XSS token theft.
CSRF Protection
Double-Submit Cookie Pattern
The platform uses the double-submit cookie pattern for CSRF protection:
- On login/refresh, the server sets a CSRF token in a non-HttpOnly cookie
- The frontend reads this cookie via
document.cookie - For state-changing requests (POST, PUT, PATCH, DELETE), the frontend includes the token as an
X-CSRF-Tokenheader - The server validates that the cookie value matches the header value using constant-time comparison
Why This Works
An attacker cannot forge cross-site requests because:
- They cannot read the CSRF cookie from another origin (same-origin policy)
- They cannot set custom headers on cross-origin requests without CORS
- Without the cookie value, they cannot set the
X-CSRF-Tokenheader
Client-Side CSRF Handling (csrf.ts)
// Read CSRF token from cookies
export function getCSRFToken(): string | null {
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'csrf_token') return decodeURIComponent(value);
}
return null;
}
// Fetch wrapper that auto-includes CSRF header
export async function csrfFetch(url: string, options: RequestInit): Promise<Response> {
const headers = new Headers(options.headers);
const token = getCSRFToken();
if (token) headers.set('X-CSRF-Token', token);
return fetch(url, { ...options, headers, credentials: 'include' });
}CSRF Token Lifecycle
- Generated on successful login
- Rotated on each token refresh (prevents replay with stale tokens)
- Cleared on logout
- Fetched via
GET /api/auth/csrf-tokenif missing (e.g., after page refresh before first state-changing request)
Session Management
Session Lifecycle
| Event | Access Token (15 min) | Refresh Token (7 days) | In-Memory State |
|---|---|---|---|
| Login | Set in cookie | Set in cookie | isAuthenticated: true |
| Page refresh | Cookie persists | Cookie persists | Lost, restored via checkSession() |
| Suite navigation | Auto-attached by browser | Auto-attached on /api/auth paths | New AuthProvider calls checkSession() |
| Token refresh (5 min before expiry) | New cookie set | New cookie set | accessExpiresAt updated |
| Logout | Cookie cleared | Cookie cleared | isAuthenticated: false |
| Inactivity (30 min) | N/A | N/A | Auto-logout triggered |
Proactive Token Refresh
The AuthProvider schedules a refresh 5 minutes before the access token expires:
const scheduleRefresh = (expiresAt: number) => {
const refreshIn = expiresAt - Date.now() - tokenRefreshBufferMs; // 5 min buffer
setTimeout(async () => {
const result = await refreshService(apiBaseUrl);
if (result.success) scheduleRefresh(result.accessExpiresAt);
else handleSessionExpired();
}, refreshIn);
};Refresh Queue (Mutex Pattern)
The TokenRefreshQueue prevents multiple concurrent refresh requests:
class TokenRefreshQueue<T> {
private refreshPromise: Promise<QueuedRefreshResult<T>> | null = null;
async refresh(refreshFn: () => Promise<T>): Promise<QueuedRefreshResult<T>> {
if (this.refreshPromise) return this.refreshPromise; // Re-use in-flight request
this.refreshPromise = this.doRefresh(refreshFn);
return this.refreshPromise;
}
}Inactivity Timeout
The AuthProvider tracks user activity (mouse, keyboard, scroll, touch) and logs the user out after 30 minutes of inactivity:
- Activity events are throttled (1 event per second)
- The inactivity timer is reset on each activity
- On timeout,
logout()is called and the user is redirected to login
credentials: 'include'
All fetch requests to the oracle-bridge use credentials: 'include':
// Login (plain fetch)
fetch(url, { method: 'POST', credentials: 'include', ... });
// Session check
fetch(url, { method: 'GET', credentials: 'include' });
// Refresh and logout (via csrfFetch wrapper)
export async function csrfFetch(url, options) {
return fetch(url, { ...options, credentials: 'include' });
}This is required for the browser to:
- Send cookies with cross-origin requests
- Accept
Set-Cookieheaders from the server
The server must respond with Access-Control-Allow-Credentials: true in its CORS configuration.
Internet Identity Integration
Internet Identity (II) authentication uses a different flow from email/password:
- User initiates II authentication via
@dfinity/auth-client - II returns a delegation chain (not a password)
- The delegation is verified by the auth-service canister
- On success, the same cookie-based session is established
The cookie-based session management is provider-agnostic. Once authenticated through any provider (EmailPassword, II, Google, etc.), the session uses the same HttpOnly cookie mechanism.
Suite-by-Suite Auth Implementation
@hello-world-co-op/auth Package (Shared)
The canonical auth implementation. Suites should consume this package for:
AuthProvider- React context provider with session managementProtectedRoute- Route guard with login redirectuseAuth()- Hook for accessing auth stateauthServiceClient- Direct API client for login/logout/refresh/checkSession
think-tank-suite
Pattern: AuthProviderBridge (nanostores to @hello-world-co-op/auth context)
The think-tank-suite has a dual-mode auth system:
- authStore (nanostores): Manages auth state internally, supports both cookie-based (
authCookieClient) and legacy localStorage modes viaisCookieAuthEnabled()feature flag - AuthProviderBridge: Wraps the nanostore state to provide
AuthContextValuecompatible with@hello-world-co-op/auth - Cookie mode: Enabled by default in production (
VITE_USE_COOKIE_AUTH=true). Usescredentials: 'include'and CSRF protection - ProtectedRoute: Uses nanostores directly (
$isAuthenticated,$isLoading)
Status: Fully compatible with cross-suite SSO when cookie mode is enabled.
governance-suite
Pattern: Same as think-tank-suite (nanostores + authCookieClient)
- Uses its own
authCookieClient.ts(copy of dao-suite pattern) - Has
authStorewithisCookieAuthEnabled()feature flag ProtectedRouteuses nanostores directlycredentials: 'include'in all fetch calls
Status: Fully compatible with cross-suite SSO when cookie mode is enabled.
dao-suite
Pattern: Dual-mode (legacy localStorage + cookie-based client)
- Legacy auth (
utils/auth.ts): Stores tokens inlocalStorage- this is a known gap flagged asFE-DAO-1 (CRITICAL)with TODO to migrate - Cookie client (
services/authCookieClient.ts): Newer cookie-based implementation usingcredentials: 'include'and CSRF - ProtectedRoute: Currently uses the legacy localStorage
isAuthenticated()andgetRefreshToken()fromutils/auth.ts - authHelpers.ts: Reads
user_datafrom localStorage
Gap: The ProtectedRoute component still uses the legacy localStorage-based auth, not the cookie client. This means dao-suite may not participate in cross-suite SSO until fully migrated to cookie auth. The cookie client exists but is not yet the default code path for route protection.
marketing-suite
Pattern: No authentication (public-only)
- No auth imports found in source code
- All pages are publicly accessible
- No
AuthProviderorProtectedRoute
Status: N/A - no auth required.
otter-camp-suite
Pattern: Standalone localStorage reader
authHelpers.tsreadsuser_datafromlocalStoragefor simple auth checks- No dependency on
@hello-world-co-op/auth - No cookie-based auth integration
- Minimal auth footprint (just checks if
user_dataexists with an access token)
Gap: Does not use cookie-based auth. If cross-suite SSO is needed for the game, it would need to be integrated with the shared auth package or at minimum use the cookie session check.
Identified Gaps
Gap 1: Missing AUTH_COOKIE_DOMAIN in oracle-bridge .env.example
Severity: Medium Impact: Developers may not configure cookie domain, breaking SSO in staging/production Location: /home/coby/git/oracle-bridge/.env.exampleFix: Add AUTH_COOKIE_DOMAIN, AUTH_COOKIE_SECURE, and AUTH_COOKIE_SAME_SITE to .env.example
Gap 2: dao-suite ProtectedRoute Uses Legacy localStorage Auth
Severity: Medium (known, tracked as FE-DAO-1) Impact: dao-suite route protection does not participate in cross-suite SSO Location: /home/coby/git/dao-suite/src/components/ProtectedRoute.tsxMitigation: The authCookieClient.ts exists in dao-suite and is ready for migration. The ProtectedRoute needs to be updated to use cookie-based session checks.
Gap 3: otter-camp-suite Uses Standalone localStorage Auth
Severity: Low (game may not need full SSO) Impact: Game suite cannot detect cross-suite sessions Location: /home/coby/git/otter-camp-suite/src/utils/authHelpers.tsMitigation: If SSO is needed for the game, integrate @hello-world-co-op/auth or add cookie-based session check.
Gap 4: SameSite Default is 'strict'
Severity: Low-Medium Impact: Cross-subdomain navigation from external links may not carry cookies with SameSite=strict. Users clicking a link to dao.helloworlddao.com from an email would not have their cookies sent, requiring re-login. Recommendation: Use AUTH_COOKIE_SAME_SITE=lax in production for better SSO UX.
Troubleshooting Guide
Issue 1: Cookies Not Shared Across Suites
Symptoms: User logs in on one suite but appears unauthenticated on another suite.
Diagnosis:
- Open browser DevTools > Application > Cookies
- Check the domain of auth cookies
- If domain shows exact hostname (e.g.,
www.helloworlddao.com) instead of.helloworlddao.com, cookies are not shared
Fix: Set AUTH_COOKIE_DOMAIN=.helloworlddao.com in oracle-bridge environment variables.
Issue 2: Session Lost on Page Refresh
Symptoms: User is authenticated, refreshes the page, and appears logged out momentarily.
Diagnosis: This is expected behavior. In-memory session metadata is lost on refresh. The AuthProvider calls checkSession() on mount to restore state from cookies.
Fix: Ensure isLoading state is handled in the UI (show loading indicator during session restoration). The ProtectedRoute component handles this automatically.
Issue 3: CORS Errors on Auth Requests
Symptoms: Console shows Access to fetch at '...' has been blocked by CORS policy.
Diagnosis:
- Check oracle-bridge
CORS_ORIGINSincludes the suite's origin - Check that
Access-Control-Allow-Credentials: trueis in the response - Verify
credentials: 'include'is set in the fetch call
Fix:
- Add the suite's origin to
CORS_ORIGINSin oracle-bridge config - Ensure the CORS middleware sends
Access-Control-Allow-Credentials: true
Issue 4: SameSite=Strict Blocks Cookies on Navigation
Symptoms: User is authenticated but loses session when clicking a link from an email or external site to a suite.
Diagnosis: SameSite=strict prevents cookies from being sent on top-level navigations initiated from a different site.
Fix: Set AUTH_COOKIE_SAME_SITE=lax in oracle-bridge. Lax allows cookies on top-level GET navigations while still preventing CSRF on POST requests.
Issue 5: Secure Flag Requires HTTPS
Symptoms: Cookies are not set in local development using http://localhost.
Diagnosis: The Secure flag requires HTTPS. In development, AUTH_COOKIE_SECURE defaults to false, but if manually set to true, cookies will not be set over HTTP.
Fix: For local development, ensure AUTH_COOKIE_SECURE is not set to true, or use HTTPS with a self-signed certificate.
Issue 6: CSRF Token Missing
Symptoms: State-changing requests (POST/PUT/DELETE) fail with 403 Forbidden - CSRF token required.
Diagnosis:
- Check if
csrf_tokencookie exists in DevTools - Check if
X-CSRF-Tokenheader is included in the request - CSRF cookie may be missing after initial page load (before login)
Fix: The csrfFetch() wrapper calls ensureCSRFToken() which fetches a token from GET /api/auth/csrf-token if none exists. Ensure all state-changing requests use csrfFetch() instead of plain fetch().
Issue 7: Token Refresh Race Condition
Symptoms: Multiple 401 errors in quick succession, or concurrent refresh requests.
Diagnosis: Multiple parts of the application detect an expired token and try to refresh simultaneously.
Fix: This is handled by the TokenRefreshQueue mutex. Only the first refresh request executes; subsequent callers receive the same promise result. Verify the refresh queue is being used (via authServiceClient.refreshTokens(), not direct fetch calls).
Debugging with Browser DevTools
Application Tab > Cookies:
- Verify
access_token,refresh_token,csrf_tokenare present - Check
Domainis.helloworlddao.com(not exact hostname) - Verify
HttpOnlyflag on access_token and refresh_token - Check
SecureandSameSiteattributes
Network Tab:
- Filter by
/api/authto see auth requests - Check
Cookierequest header includes auth cookies - Check
Set-Cookieresponse headers for cookie attributes - Verify
credentials: includein request details
Console Tab:
- Auth service logs session initialization
- Token refresh attempts with success/failure
- Cookie configuration warnings at oracle-bridge startup
- CSRF token read/fetch events
Configuration Reference
Auth Package (@hello-world-co-op/auth) Config
const config: AuthConfig = {
apiBaseUrl: 'https://oracle.helloworlddao.com', // oracle-bridge URL
inactivityTimeoutMs: 30 * 60 * 1000, // 30 minutes
tokenRefreshBufferMs: 5 * 60 * 1000, // 5 minutes before expiry
onSessionExpired: (returnUrl) => { ... }, // Custom handler
};Oracle-Bridge Environment Variables
# Auth cookie configuration
AUTH_COOKIE_DOMAIN=.helloworlddao.com # Required for cross-suite SSO
AUTH_COOKIE_SECURE=true # Always true in production
AUTH_COOKIE_SAME_SITE=lax # 'lax' recommended for SSO
# CORS (must include all suite origins)
CORS_ORIGINS=https://helloworlddao.com,https://www.helloworlddao.com,https://staging.helloworlddao.com
# Auth service canister
AUTH_SERVICE_CANISTER_ID=<canister-id>
IC_HOST=https://ic0.appSuite Environment Variables
# Oracle-bridge URL for auth API calls
VITE_ORACLE_BRIDGE_URL=https://oracle.helloworlddao.com
# Cookie auth mode (think-tank-suite, governance-suite)
VITE_USE_COOKIE_AUTH=true # Default: true in production