This page is part of the wikibot.io PRD (Product Requirements Document). See also: Design/Platform Overview, Design/Data Model, Design/Implementation Phases, Design/Operations.


Auth & Multi-tenancy

User registration and login

OAuth-only login — no email/password accounts. Users sign in with Google, GitHub, Microsoft, or Apple. This eliminates password management, credential stuffing, and throwaway-account spam (every user needs a real OAuth identity). Combined with tier limits (1 wiki, 500 pages), this provides sufficient abuse prevention without rate limiting on the free tier.

Auth provider: WorkOS AuthKit

Why WorkOS: 1M MAU free tier (vs Clerk's 50K MRU or Cognito's 10K MAU). All four OAuth providers supported. Clean hosted UI (AuthKit). First-class MCP OAuth 2.1 support with a documented FastMCP integration. Python/Flask examples. The free tier is a loss leader for enterprise SSO/SCIM sales ($125/connection/month) — it's strategically sustainable, not charity.

WorkOS cost curve (2026 pricing): Free to 1M MAU. Beyond that, $2,500/mo per additional 1M block. Custom domain is \(99/mo (deferrable — the default `*.authkit.app` subdomain is unbranded). For comparison, Clerk at 100K users costs ~\)1,825/mo; WorkOS costs $0.

Two token flows (important — these are distinct):

  1. Browser login: User authenticates via WorkOS AuthKit → our /auth/token endpoint issues a platform JWT (signed with our own RS256 key in Secrets Manager) → all subsequent browser/API requests validate against our key. WorkOS is not in the runtime path.

  2. MCP OAuth (Claude.ai): WorkOS acts as the OAuth 2.1 Authorization Server via its Standalone Connect flow. Claude.ai authenticates with WorkOS → WorkOS issues its own access token → our MCP endpoint validates against WorkOS JWKS. FastMCP has a documented WorkOS integration that handles DCR, PKCE, AS metadata, and token validation.

Outage blast radius: WorkOS outage blocks new logins and new MCP OAuth connections. Active browser sessions (platform JWT) and existing Claude Code bearer tokens are unaffected. Claude.ai MCP connections drop when the WorkOS access token expires and refresh fails — token lifetime determines the window.

Migration path off WorkOS: We store oauth_provider + oauth_provider_sub (raw provider subject ID, retrieved via WorkOS API during first login). This enables migration to:

  • Self-hosted OAuth with authlib — handles browser login (2-3 days). But also requires building an OAuth 2.1 AS for MCP (DCR, PKCE, token endpoint, JWKS) — realistically 1-2 weeks total, unless FastMCP's built-in auth providers can replace it.
  • Clerk — 50K MRU free tier, MCP support added mid-2025, free custom domain.
  • Cognito — free, native AWS, ugly UI, 10K MAU free tier.

Apple provider sub caveat: WorkOS exposes raw provider sub claims via API for Google, GitHub, and Microsoft. Apple support for this is undocumented — verify in Phase 0 that we can retrieve Apple's raw sub. If not, Apple users would need to re-authenticate during a provider migration.

Design rules:

  • Browser/API middleware validates platform JWTs (our key). Never touches WorkOS at runtime.
  • MCP middleware validates WorkOS access tokens (WorkOS JWKS). FastMCP integration handles this.
  • Both paths converge on the same ACL check: resolve user → look up permissions in DynamoDB → set Otterwiki headers.

Wiki-level auth

Three access methods, all converging on the same ACL check:

  1. Browser session (web UI): Platform JWT → middleware validates against our RS256 key → extracts user identity → checks ACL in DynamoDB → sets Otterwiki headers
  2. MCP OAuth (Claude.ai): WorkOS access token → middleware validates against WorkOS JWKS (via FastMCP integration) → extracts user identity → checks ACL → sets Otterwiki headers
  3. Bearer token (Claude Code, API clients): Token in Authorization header → middleware hashes token, looks up in Wikis table → resolves to user + wiki → checks ACL

MCP auth

Two paths:

Claude.ai (OAuth 2.1): WorkOS acts as the Authorization Server. FastMCP's WorkOS integration handles the plumbing (DCR, PKCE, AS metadata, token validation). The MCP endpoint serves /.well-known/oauth-protected-resource pointing to WorkOS. Claude.ai discovers this, authenticates via WorkOS, and presents the access token to the MCP endpoint. Per-wiki authorization happens in our middleware, not in WorkOS — WorkOS identifies the user, we check whether they can access the wiki.

Claude Code / API clients (bearer token): Each wiki gets a unique MCP bearer token, generated at wiki creation time, stored as a bcrypt hash in DynamoDB. The user sees the token once (at creation) and can regenerate it from the dashboard. Usage: claude mcp add --transport http.

MCP endpoint URL: https://{username}.wikibot.io/{wiki}/mcp.

ACL model

Simple role-based model:

Role Read Write Delete Manage ACL Delete wiki
viewer yes no no no no
editor yes yes yes no no
owner yes yes yes yes yes

Wiki creator is always owner. Owners can grant viewer/editor access to other registered users (by email). Free tier: up to 3 collaborators. Premium tier: up to 25.


AAA Model (Authentication, Authorization, Accounting)

Authentication (who are you?)

All authentication happens at the platform layer. Otterwiki never sees credentials — it runs in PROXY_HEADER auth mode and trusts headers from the platform middleware.

Four entry points, all converging to the same identity:

Browser        → OAuth (Google/GitHub/Microsoft/Apple) → JWT
Claude Code    → MCP bearer token
Claude.ai      → OAuth 2.1 → JWT
Git CLI        → Git credential (bearer token)

All paths → platform middleware → resolves to User record in DynamoDB

The platform middleware is the single authentication boundary. Everything downstream trusts it.

Authorization (what can you do?)

Layer 1 — Platform middleware (before Otterwiki sees the request):

  1. Resolve user identity from JWT or bearer token
  2. Resolve wiki from {username}.wikibot.io/{wiki_slug}
  3. Look up ACL: does this user have a grant on this wiki?
  4. If no grant and wiki is not public → 403
  5. Check tier limits (page count, attachment size) on writes
  6. Map ACL role to Otterwiki permission headers:
ACL role x-otterwiki-permissions header
viewer READ
editor READ,WRITE,UPLOAD
owner READ,WRITE,UPLOAD,ADMIN
anonymous (public wiki) Synthetic user with READ only
  1. Set headers: x-otterwiki-email, x-otterwiki-name, x-otterwiki-permissions
  2. Forward to Otterwiki

Layer 2 — Otterwiki (AUTH_METHOD=PROXY_HEADER):

  • Reads headers, creates ephemeral user object per request
  • No local user database — all identity comes from headers
  • Enforces READ/WRITE/UPLOAD/ADMIN based on the permissions header
  • Owner gets the admin panel; editors can edit; viewers can read

For MCP and API paths, Otterwiki is not involved in auth — the handlers read the git repo directly. Authorization happens entirely in Layer 1.

Public wiki access

When a wiki is set to public, unauthenticated visitors need read access. Rather than changing Otterwiki's READ_ACCESS config per wiki, the platform middleware injects a synthetic anonymous user with READ permission for public wikis. Otterwiki config stays identical for all wikis:

AUTH_METHOD = "PROXY_HEADER"
READ_ACCESS = "APPROVED"          # always — public access handled by middleware
WRITE_ACCESS = "APPROVED"
ATTACHMENT_ACCESS = "APPROVED"
DISABLE_REGISTRATION = True       # no Otterwiki-level registration

Per-wiki Otterwiki configuration

Each wiki needs its own Otterwiki config. Platform-managed settings (set by the platform, not the user):

AUTH_METHOD = "PROXY_HEADER"
READ_ACCESS = "APPROVED"
WRITE_ACCESS = "APPROVED"
ATTACHMENT_ACCESS = "APPROVED"
DISABLE_REGISTRATION = True

User-configurable settings (via Otterwiki admin panel, stored in the wiki's config):

SITE_NAME = "Third Gulf War"      # wiki display name
SITE_DESCRIPTION = "..."          # meta description
SITE_LOGO = "..."                 # custom logo URL
# sidebar config, content/editing prefs, etc.

Otterwiki Admin Panel — Section Disposition

The wiki owner (ACL role owner) gets ADMIN permission, which grants access to Otterwiki's admin panel at /-/admin/*. Some sections are useful; others conflict with platform-managed settings and must be disabled.

Admin section Route Disposition Reason
Application Preferences /-/admin Keep Wiki branding: site name, description, logo, favicon, language, custom home page, robot crawlers
Sidebar Preferences /-/admin/sidebar_preferences Keep UI layout: sidebar shortcuts, custom menu items, page index mode/focus
Content and Editing /-/admin/content_and_editing Keep Git workflow: commit message mode/template, page name casing, underscore handling, WikiLink style
Repository Management /-/admin/repository_management Disable Conflicts with platform Git management. Git web server, remote push/pull, SSH keys — all managed by platform. Premium external Git sync replaces this.
Permissions and Registration /-/admin/permissions_and_registration Disable Conflicts with platform auth. READ/WRITE/ATTACHMENT levels, registration, approval, email confirmation — all managed by platform middleware.
User Management /-/admin/user_management Disable No local user database in ProxyHeaderAuth mode. Add/edit/delete user forms are non-functional. User management happens in the platform dashboard (ACL grants).
Mail Preferences /-/admin/mail_preferences Disable for MVP SMTP configuration for notifications. Not relevant until we add wiki-level email notifications (e.g., notify collaborators on page edit). Could re-enable as a premium feature later.

Implementation: Override the admin navigation template to hide disabled sections. Return 404 from disabled routes in middleware (defense in depth — don't rely solely on hiding the links). This is a small fork change: modify templates/settings.html to conditionally render nav items, and add a decorator or middleware check on the disabled routes.

Accounting (resource tracking and tier enforcement)

All enforcement happens in Layer 1 middleware, before Otterwiki runs.

Resource Where tracked Checked on
Wiki count per user Metadata store (User.wiki_count) Wiki creation
Page count per wiki Metadata store (Wiki.page_count) Page creation (write path)
Attachment total size per wiki Metadata store (updated on upload) Attachment upload
Single attachment size Computed from request Attachment upload
Collaborator count per wiki Metadata store (count ACL grants) ACL grant
Semantic search access Metadata store (Wiki.semantic_search_enabled) Semantic search request
REST API access Metadata store (User.tier) Any /api/v1/* request
Custom domain Metadata store (User.tier) Domain configuration
Git write access Metadata store (User.tier) git push (receive-pack)

A free user hitting the 500-page limit gets a clear error ("Upgrade to premium for unlimited pages") before any git operation occurs. Tier checks are fast (single DynamoDB read, cached in warm Lambda).