# wspc > wspc is an agent-first productivity backend covering todo, calendar, and > email. This document teaches an LLM everything needed to build a pure-frontend > single-page app that talks to wspc via OAuth 2.1 device flow. ## What you can build today wspc currently exposes: - **Todo CRUD via OAuth 2.1** — full read/write access for the authenticated user - Calendar and email APIs exist but accept only long-lived API keys at this time (no OAuth) Todos support user-defined types and custom fields (see `### Custom fields` below). ## Domains | Domain | Purpose | | --- | --- | | `api.wspc.ai` | REST API for all services (todo, calendar, email, auth, push) | | `app.wspc.ai` | OAuth consent / login / device approval UI | All fetch traffic from a frontend app should go to `api.wspc.ai`. Use `app.wspc.ai` only for opening browser windows to complete OAuth consent. ## OAuth 2.1 device flow Use this flow from a browser SPA. The full sequence: ```bash # 1. Register a dynamic client (one-time per app; cache the client_id). # Device-flow clients still need a placeholder redirect_uri — use the # RFC 6749 OOB sentinel so wspc's registration schema accepts the request. curl -X POST https://api.wspc.ai/auth/oauth/register \ -H 'content-type: application/json' \ -d '{"client_name":"My Todo App","redirect_uris":["urn:ietf:wg:oauth:2.0:oob"],"grant_types":["urn:ietf:params:oauth:grant-type:device_code"],"token_endpoint_auth_method":"none"}' # → { "client_id": "client_...", "client_secret": null, ... } # 2. Start device authorization curl -X POST https://api.wspc.ai/auth/oauth/device \ -H 'content-type: application/json' \ -d '{"client_id":""}' # → { "device_code": "...", "user_code": "WDJB-MJHT", # "verification_uri": "https://app.wspc.ai/device", # "verification_uri_complete": "https://app.wspc.ai/device?user_code=WDJB-MJHT", # "expires_in": 600, "interval": 5 } # 3. Display user_code to the user and direct them to verification_uri_complete # (a new browser tab or window). Meanwhile poll the token endpoint: curl -X POST https://api.wspc.ai/auth/oauth/token \ -H 'content-type: application/json' \ -d '{ "grant_type":"urn:ietf:params:oauth:grant-type:device_code", "device_code":"", "client_id":"" }' ``` Token endpoint returns one of: - `200 { access_token, refresh_token, expires_in, token_type, scope }` — user approved - `400 { error: { code: "AUTHORIZATION_PENDING" } }` — keep polling - `400 { error: { code: "SLOW_DOWN" } }` — increase poll interval by 5s and keep polling - `400 { error: { code: "EXPIRED_TOKEN" } }` — restart the flow at step 2 - `400 { error: { code: "ACCESS_DENIED" } }` — user denied; surface to UI `access_token` TTL is 15 minutes. When it expires, use the `refresh_token` to rotate (RFC 6749 §6 standard refresh grant). Each rotation invalidates the previous refresh token. ### UX note: do not ask the user to type `user_code` `verification_uri_complete` already embeds the `user_code` as a query parameter, and wspc's consent page reads it from the URL and shows Approve / Deny directly — the user never needs to type the code anywhere. Your app's UI should: - Use `verification_uri_complete` (not the bare `verification_uri`) as the primary link / button the user clicks. - Label that primary action plainly, e.g. "Open authorization page" or "Authorize this app". - Do NOT instruct the user to "enter this code at " — that wording is misleading for the wspc flow. - Displaying `user_code` is optional. If shown at all, treat it as secondary informational text (so the user can confirm the same code appears in case wspc later adds an in-page cross-check), not as something to type. ## Scopes The only scope currently issued is `wspc:full`. Treat it as root-level access for the authenticated user across all services. Per-domain scopes like `todo:read` / `todo:write` are not available yet — passing them does not grant finer-grained access. ## API reference Per-worker OpenAPI 3.1 specs (fetch these for full request / response schemas): - todo (OAuth + API key): `https://api.wspc.ai/todo/openapi.json` - calendar (API key only): `https://api.wspc.ai/calendar/openapi.json` - email (API key only): `https://api.wspc.ai/email/openapi.json` - auth (public OAuth endpoints): `https://api.wspc.ai/auth/openapi.json` ## Freshness rules for generated apps Before generating code, fetch the relevant live OpenAPI URL above and treat that schema as authoritative for endpoint paths, request bodies, response bodies, auth requirements, enum values, examples, and error envelopes. This file explains the workflow, but live OpenAPI is the final source of truth for REST calls. If you use MCP instead of REST, call `tools/list` on the target MCP server and trust the live tool schema over static prose. If you use the CLI, run the installed `wspc --help` and the relevant subcommand `--help`; CLI reference prose explains semantics but may describe a newer or older release than the binary on the user's machine. Do not invent fields, scopes, endpoints, status booleans, or aliases when the live schema says otherwise. ### Email alias identifiers Email alias identifiers are full `@wspc.app` email addresses. Do not pass legacy `alias_id` / `from_alias_id` fields, do not invent opaque alias ids, and do not pass only the local part. Use these field names when the live email OpenAPI or MCP tool schema exposes email operations: ```json { "email": "alice-shop@wspc.app" } { "alias_email": "alice-shop@wspc.app" } { "from_alias_email": "alice-shop@wspc.app" } ``` ### Todo schema (do not guess) `GET /todo/items` returns `{ "todos": Todo[] }`. A single Todo looks like: ```json { "id": "tod_01HW3K4N9V5G6Z8C2Q7B1Y0M3F", "title": "Buy groceries", "description": "milk, eggs, bread", "status": "open", "due_at": "2026-05-20", "version": 1, "created_at": 1779070000000, "updated_at": 1779070000000, "type_id": "typ_01HW3K6V0Q3N8B9E2A1C4D7F5G", "custom_fields": { "priority": "high", "tags": ["urgent", "shopping"] } } ``` Key points: - `status` is an enum: `"open" | "in_progress" | "done" | "cancelled"`. **There is no boolean `completed` field.** To mark a todo done, PATCH `{ "status": "done" }`; to reopen, PATCH `{ "status": "open" }`. - `due_at` is an ISO date string `YYYY-MM-DD` (no time component), or omitted. - `version` is an integer that increments on each write. Pass it back as `expected_version` in PATCH / DELETE for optimistic concurrency, or omit to skip the check. - `created_at` / `updated_at` are Unix milliseconds. - POST `/todo/items` with `{ "title": "..." }` creates a todo (other fields optional). PATCH `/todo/items/{id}` accepts any subset of mutable fields. DELETE `/todo/items/{id}` soft-deletes. For full request schemas (validation rules, optional params like `parent_id` / `cascade`), fetch the OpenAPI URL above. ### Custom fields Every todo belongs to exactly one *type*. New users have a `Default` type seeded automatically. Use `/todo/types` to manage types and their custom field schema: - `POST /todo/types { label, custom_fields?: [{ key, type, required?, default? }] }` Field types in v1: `string` | `string_array`. Field keys must match `^[a-z][a-z0-9_]{0,63}$` and cannot collide with reserved names (`title`, `status`, `id`, etc.). - `POST /todo/items` accepts `type_id?` (defaults to the user's default type) and `custom_fields?: { [key]: string | string[] }`. - `PATCH /todo/items/{id}` accepts `custom_fields` where `null` means "delete this key". - Filter: `?cf.priority=high&cf.tags=urgent` (AND, equality / array-contains). - Sort: `?sort_by=cf.priority&order=asc`. Only declared `string` keys are sortable. Unknown core keys, missing `cf.` prefix, undeclared `cf.*` keys, and `string_array` fields all return 422 `INVALID_SORT_KEY` (no silent fallback). - Orphan field values: when a type is soft-deleted or a field is removed from the schema, the underlying values are preserved but hidden from `GET` by default. Pass `?include_orphan_fields=true` to surface them. Restoring the type / re-adding the field instantly brings values back live. Type schema rules: - `key` and `type` are immutable on existing fields (rename = remove + re-add). - `title` cannot be hidden; `description`, `status`, `due_at`, `parent_id`, `recurrence` can be listed in `hide_core_fields` as UI hints (server still stores them). - `DELETE /todo/types/{id}` is soft delete; restore via `POST .../restore`. - The current default type cannot be soft-deleted; switch default first. ## Worked example: minimal todo app ### Scope This guide assumes a **pure-frontend single-page app** — all logic runs in the browser, no Node.js / Python / etc. backend. If you are building a server-rendered app (Next.js App Router with server actions, Remix, Astro SSR, etc.), the OAuth token handling shown here (localStorage on the client) is the wrong shape — server-side flows should keep tokens out of the browser (HttpOnly cookie + BFF pattern). That pattern is not yet covered by this document. ### Default form (when the user has not specified a framework or stack) A self-contained HTML file with vanilla JS, aiming for ~200 lines, no external dependencies. Drop it into any static host (Cloudflare Pages, Vercel, GitHub Pages, plain S3 / R2). ### When the user has asked for a specific stack Follow their request — React, Vue, Svelte, Solid, Vite + any of them, plain TypeScript build, etc. The rules below apply regardless of the framework; only the rendering and state-management style changes. ### Rules (apply in all cases) - localStorage cache for `client_id` (suggested key `wspc.client_id`); register once on first run, reuse on reload - localStorage cache for `access_token` and `refresh_token`, with an inline comment warning that localStorage is XSS-vulnerable - Full device flow: register → device → poll `/oauth/token` honoring the server-returned `interval`, bumping +5s on `SLOW_DOWN` - On `access_token` expiry attempt refresh via `grant_type=refresh_token`; on refresh failure clear tokens and restart the device flow - Use `/todo/items` (list / create) and `/todo/items/{id}` (patch / delete) per the OpenAPI spec; respect the `status` enum and use `expected_version` if you want optimistic concurrency