Feature: Auth - API Tokens
Version: 1.0.0 Last Reviewed: 2026-02-10 Status: Approved
User Story
As a developer/parent, I can generate an API token for programmatic access (Home Assistant, scripts, etc.) so I can integrate baby tracking into my home automation.
MVP Scope
- Generate a personal API token (random string with
bb_prefix) - Optionally name the token (e.g., "Home Assistant", "Shortcut")
- List active tokens (metadata only, never the token value)
- Revoke a token
- Token works as Bearer auth header (same as session tokens)
NOT in MVP
- Token scopes/permissions (all tokens have full access)
- Token expiration (tokens are permanent until revoked)
- Rate limiting per token
API Contract
Generate Token
POST /api/v1/auth/tokens
Authorization: Bearer <token>
Request:
{
"name": "Home Assistant" // optional, defaults to "API Token"
}
Response 201:
{
"api_token": {
"id": "uuid",
"token": "bb_abc123def456...",
"name": "Home Assistant",
"created_at": "2026-02-10T12:00:00.000Z"
}
}
Note: The plain `token` value is shown ONLY in this response.
It is stored as a SHA-256 hash in the database.
The user must save it immediately - it cannot be retrieved later.
List Tokens
GET /api/v1/auth/tokens
Authorization: Bearer <token>
Response 200:
{
"api_tokens": [
{
"id": "uuid",
"name": "Home Assistant",
"created_at": "2026-02-10T12:00:00.000Z",
"last_used": "2026-02-10T15:30:00.000Z"
}
],
"count": 1
}
Note: Token value is NEVER returned in list responses.
Revoke Token
DELETE /api/v1/auth/tokens/:id
Authorization: Bearer <token>
Response 204: (No Content)
Response 404:
{ "error": { "code": "NOT_FOUND", "message": "Token not found", "details": [] } }
Error Responses
Standard error format. Error codes: 400 VALIDATION_ERROR, 401 UNAUTHORIZED, 404 NOT_FOUND
Token Format
- Prefix:
bb_(Baby Basics) for easy identification in config files - Body:
crypto.randomBytes(32).toString('hex')(64 hex chars) - Full token:
bb_<64-hex-chars>(67 chars total) - Storage: SHA-256 hash of the full token string stored in DB
- Lookup: On auth, hash the incoming token, look up the hash
Security
- Plain token shown only once on creation
- Stored as SHA-256 hash (not reversible)
last_usedupdated on each authenticated request (fire-and-forget, non-blocking)- Revoking a token immediately invalidates it
- Users can only see/revoke their own tokens
- API tokens authenticate identically to session tokens - you can manage tokens from either auth method
Audit Logging
- Token created: AuditLog entry with entity_type="api_token", action="create", changes={name} (never the token value)
- Token revoked: AuditLog entry with entity_type="api_token", action="delete", changes={name}
Acceptance Criteria
- Can generate a new API token (wrapped response)
- Token has
bb_prefix - Token is stored as SHA-256 hash (not plaintext)
- Can list all active tokens (wrapped with count)
- Token value is NOT included in list response
- Can revoke a token by ID (returns 204)
- Revoked token returns 401 on subsequent use
- API token works as Bearer auth for all authenticated endpoints
-
last_usedupdates when token is used for authentication - Cannot see or revoke another user's tokens (404)
Test Cases
- Generate: → 201, {api_token: {token: "bb_..."}}
- Authenticate with token: use generated token → success
- List tokens: generate 2, list → {api_tokens: [...], count: 2}, no token values
- Revoke: revoke token → 204, use it → 401
- Ownership: User A generates, User B tries to revoke → 404
- Last used: use token, list → last_used is recent
- Named token: name="Test" → list shows "Test"
- Default name: no name → list shows "API Token"
- Hash verification: check DB → token column is hex hash, not bb_ prefix
- Manage via API token: authenticate with API token A, revoke API token B → 204
Boundaries
- No token scopes (full access)
- No token expiration
- No limit on number of tokens per user
- API tokens and session tokens are interchangeable for authentication