Properties
category: reference tags: - phase-2 - username - routing last_updated: 2026-03-14
P2-2/3: Username-Based URLs for Multi-Tenant Routing
Summary
Added username field to the Users table and updated the resolver to route by {username}.wikibot.io instead of {user_id}.wikibot.io. Internal storage paths (EFS, wiki_id, ACL keys) remain UUID-based.
Changes
Infrastructure (infra/components/dynamodb.py)
- Added
usernameattribute (type S) to Users table - Added
username-indexGSI (hash_key:username, projection: ALL)
User Model (app/models/user.py)
RESERVED_USERNAMES— 18 reserved names (admin, www, api, dev, mcp, wiki, etc.)validate_username(username)— lowercase alphanumeric + hyphens, 3-30 chars, no leading/trailing hyphens; returns(valid, error)get_by_username(username)— GSI query onusername-indexset_username(user_id, username)— validates format, checks reserved list, checks uniqueness via GSI, then updates
Auth Middleware (app/auth/middleware.py)
exchange_auth_code()return dict now includesneeds_username: bool(true when user has no username set)
Management API (app/management/routes.py)
- New endpoint:
POST /admin/usernamewith body{"username": "..."} - Returns 200 on success, 400 for invalid/reserved, 409 for taken
- Normalizes input to lowercase before validation
CLI (app/cli/main.py)
- After
wikibot login, ifneeds_usernameis true, prompts user for username and callsPOST /admin/username _set_username()helper handles the API call and output
Resolver (app/otterwiki/resolver.py)
_parse_host()now extracts username (not user_id) from subdomain — docstrings updated__call__()usesget_by_username(username)instead ofget(user_id)- Extracts
user_idfrom the looked-up user record for downstream UUID-based paths - 404 if username not found
Test Coverage
- Username validation: valid formats, too short/long, leading/trailing hyphens, uppercase, special chars, reserved names, empty
- GSI lookup:
get_by_usernamefound and not found - set_username: success, invalid format, reserved, duplicate, idempotent
- POST /admin/username: success, invalid, reserved, taken, unauthenticated, empty, case normalization
- needs_username: new user gets
true, existing user with username getsfalse - Resolver: all existing tests updated to use username subdomain; new test verifying UUID-based internal paths
All 230 runnable tests pass. 15 pre-existing fastmcp import failures unrelated.
Branch
Committed on worktree-agent-a245f93f (commit 8d37f26), based on phase-2.