name, overview, todos, isProject
| name | overview | todos | isProject | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Public API with Keys | Build a versioned public API (v1) authenticated via shared organization-scoped automation keys reused by MCP and Public API, with course CRUD, course export, student/audience CRUD endpoints, plus a dashboard Automation page with MCP, Zapier, and API tabs. |
|
false |
Public API with Organization-Scoped API Keys
Architecture Overview
flowchart TB
subgraph client [External Client]
APIKey["API Key in Authorization header"]
end
subgraph api [API Server]
V1Router["/v1 Router"]
APIKeyMiddleware["apiKeyMiddleware"]
CourseRoutes["Course Routes"]
AudienceRoutes["Audience Routes"]
APIKeyRoutes["API Key Management"]
end
subgraph db [Database]
APIKeyTable["api_key table"]
OrgTable["organization"]
CourseTable["course / group"]
AudienceTable["groupmember / profile"]
end
client -->|"Bearer cio_..."| V1Router
V1Router --> APIKeyMiddleware
APIKeyMiddleware -->|"validates + resolves orgId"| APIKeyTable
APIKeyMiddleware --> CourseRoutes
APIKeyMiddleware --> AudienceRoutes
APIKeyRoutes -->|"session auth"| APIKeyTable
1. Database: api_key Table
Add a new table in packages/db/src/schema.ts:
export const apiKey = pgTable('api_key', {
id: uuid('id').defaultRandom().primaryKey(),
organizationId: uuid('organization_id').notNull().references(() => organization.id, { onDelete: 'cascade' }),
keyHash: text('key_hash').notNull(), // SHA-256 hash of the key
keyPrefix: varchar('key_prefix', { length: 12 }).notNull(), // "cio_live_xxxx" for display
name: varchar('name', { length: 255 }).notNull().default('Default'),
lastUsedAt: timestamp('last_used_at'),
expiresAt: timestamp('expires_at'),
isRevoked: boolean('is_revoked').notNull().default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
createdBy: uuid('created_by').notNull().references(() => profile.id),
});
- The raw key is only shown once on creation; we store a SHA-256 hash.
keyPrefix(e.g.cio_live_a1b2) lets users identify keys in the UI without exposing the secret.- DON'T GENERATE MIGRATION, when we run
pnpm db pushit will automatically apply the changes.
2. API Key Queries
New file: packages/db/src/queries/api-key/api-key.ts
Functions:
createApiKey(orgId, createdBy, name?)-- inserts row, returns idgetApiKeyByHash(hash)-- lookup for auth middlewaregetApiKeysByOrganization(orgId)-- list keys (prefix, name, createdAt, lastUsedAt, isRevoked)revokeApiKey(keyId, orgId)-- setsisRevoked = trueupdateApiKeyLastUsed(keyId)-- stampslastUsedAt
3. API Key Auth Middleware
New file: apps/api/src/middlewares/api-key.ts
- Reads
Authorization: Bearer cio_live_...header - SHA-256 hashes the key, looks up via
getApiKeyByHash - Checks
isRevoked,expiresAt - Sets
c.set('orgId', key.organizationId)on context (same pattern asorgMemberMiddleware) - Returns 401 on invalid/expired/revoked key
4. Validation Schemas
New file: packages/utils/src/validation/api-key/api-key.ts
Schemas for API key management and public API input validation:
ZCreateApiKey--{ name?: string }ZRevokeApiKeyParam--{ keyId: string (uuid) }- Public API schemas for course CRUD, audience CRUD, pagination, etc.
5. Public API Routes (Versioned)
Router structure
New directory: apps/api/src/routes/v1/
Files:
- apps/api/src/routes/v1/courses.ts -- Course endpoints
- apps/api/src/routes/v1/audience.ts -- Audience endpoints
- apps/api/src/routes/v1/index.ts -- Combines sub-routers
Register in apps/api/src/app.ts:
.route('/v1', v1Router)
All v1 routes use apiKeyMiddleware (not session auth).
Course Endpoints (/v1/courses)
| Method | Path | Description |
|---|---|---|
| GET | /v1/courses |
List all org courses (paginated) |
| POST | /v1/courses |
Create a course |
| GET | /v1/courses/:courseId |
Get course details |
| PUT | /v1/courses/:courseId |
Update a course |
| DELETE | /v1/courses/:courseId |
Soft-delete a course |
| GET | /v1/courses/:courseId/export |
Export course content (lessons, exercises) as JSON |
| GET | /v1/courses/:courseId/students |
List students enrolled in a course |
Audience Endpoints (/v1/audience)
| Method | Path | Description |
|---|---|---|
| GET | /v1/audience |
List all org students (paginated) |
| POST | /v1/audience |
Add a student to the org |
| GET | /v1/audience/:studentId |
Get student details |
| PUT | /v1/audience/:studentId |
Update student info |
| DELETE | /v1/audience/:studentId |
Remove student from org |
API Key Management (session-authenticated, not public API)
New file: apps/api/src/routes/api-key/api-key.ts
| Method | Path | Description |
|---|---|---|
| POST | /api-key |
Generate new API key (returns raw key once) |
| GET | /api-key |
List API keys for the org |
| DELETE | /api-key/:keyId |
Revoke an API key |
Registered in app.ts as .route('/api-key', apiKeyRouter). Protected by authMiddleware + orgAdminMiddleware.
6. Services
- apps/api/src/services/api-key.ts -- Key generation (crypto.randomBytes), hashing, CRUD logic
- apps/api/src/services/v1/course.ts -- Public API course business logic (reuses existing queries from
@cio/db/queries/course) - apps/api/src/services/v1/audience.ts -- Public API audience business logic (reuses
getOrganizationAudienceand related queries)
7. Tests
New test files under apps/api/src/__tests__/:
api-key.test.ts-- API key creation, listing, revocation, auth flowv1/courses.test.ts-- All course CRUD + export + students endpointsv1/audience.test.ts-- All audience CRUD endpoints
Tests will use Hono's app.request() test helper (no HTTP server needed). Mock the DB layer with vi.mock().
Additionally, include OpenAPI contract coverage:
- verify all
/v1/*and/api-key/*public-facing endpoints are represented in generated spec. - ensure schema components are generated for request/response payloads.
8. Dashboard UI: Automation Page
Sidebar Navigation
Add an "Automation" nav item in apps/dashboard/src/lib/features/ui/navigation/org-navigation.ts, placed before Settings, with requiresAdmin: true:
{
titleKey: 'org_navigation.automation',
path: '/automation',
icon: AutomationIcon, // use Zap or Workflow icon from lucide
requiresAdmin: true,
matchPattern: '^/org/[^/]+/automation(/.*)?$'
}
Route
New file: apps/dashboard/src/routes/org/[slug]/automation/+page.svelte
Page Structure
+----------------------------------------------+
| Automation |
| Connect ClassroomIO with your tools and |
| workflows. |
+----------------------------------------------+
| [ API ] [ Zapier ] [ Webhooks ] |
+----------------------------------------------+
| |
| (Tab content based on selection) |
| |
+----------------------------------------------+
- Title: "Automation" with description text
- Uses
Tabsfrom@cio/ui/base/tabs - Three tabs: API, Zapier, Webhooks
API Tab Content
Free plan users: Show UpgradeBanner component (same pattern as other pages). Disable all controls.
Paid plan users:
- API key display field (masked by default, e.g.
cio_live_••••••••••••a1b2) - "Show" / "Hide" toggle button to reveal/mask the key
- "Copy" button to copy to clipboard
- "Regenerate" button (with confirmation) to create a new key and revoke the old one
- "Read the Docs" button/link pointing to the API documentation URL
API Class
New file: apps/dashboard/src/lib/features/automation/api/automation.svelte.ts
Extends BaseApi, methods:
fetchApiKeys()-- GET/api-keycreateApiKey()-- POST/api-keyrevokeApiKey(keyId)-- DELETE/api-key/:keyId
Frontend Types
New file: apps/dashboard/src/lib/features/automation/utils/types.ts
Translations
Add keys to apps/dashboard/src/lib/utils/translations/en.json:
org_navigation.automationautomation.title,automation.descriptionautomation.tabs.api,automation.tabs.zapier,automation.tabs.webhooksautomation.api.key_label,automation.api.copy,automation.api.regenerate,automation.api.read_docsautomation.api.upgrade_promptautomation.zapier.coming_soon,automation.webhooks.coming_soon
9. Versioning Strategy
- All public API routes live under
/v1/. When v2 is needed, a newapps/api/src/routes/v2/directory is created with its own router, registered as.route('/v2', v2Router). - The v1 router remains untouched, ensuring backward compatibility.
- The API key middleware is version-agnostic and shared across all versions.
10. OpenAPI Spec Requirements
Route Annotation Requirements
All new public API routes in this PRD must be OpenAPI-documented:
- add route metadata using existing Hono OpenAPI patterns (
describeRoute, validators/schemas). - ensure request params/query/body and success/error response shapes are declared.
- avoid undocumented routes under
/v1/*and/api-key/*.
Spec Generation and Publishing
Use existing script in apps/api/scripts/upload-openapi-spec.ts:
- generate spec from app routes.
- save local output to
apps/api/dist/openapi/openapi.json. - upload dated + latest specs to R2 (
openapi/openapi-YYYY-MM-DD.json,openapi/openapi-latest.json).
Required update flow after public API changes:
- implement/adjust routes and schemas.
- run
pnpm --filter @cio/api upload:openapi. - verify
openapi-latest.jsoncontains new endpoints.
CI/Verification Gate
Add a verification step in this initiative to prevent stale API docs:
- PR checklist requires successful OpenAPI generation for API changes.
- include generated-spec validation in API CI for public API route changes.
11. Docs Integration (Fumadocs OpenAPI)
Integrate API docs in apps/docs following Fumadocs OpenAPI integration guidance:
- reference:
https://www.fumadocs.dev/docs/integrations/openapi - wire docs app to render OpenAPI spec from latest published source.
Target implementation tasks:
- Add required Fumadocs OpenAPI integration package(s) to
apps/docs/package.json. - Add a docs route/page named
APIat/apiinapps/docs/src/routes(or equivalent docs content route). - Configure the page to load
openapi-latest.jsonURL (env-driven preferred, with production fallback). - Add an
APIentry to docs navigation inapps/docs/content/docs/meta.json. - Validate local docs build still passes with the OpenAPI page.
Suggested file touchpoints:
apps/docs/package.jsonapps/docs/src/routes/*(new/apiroute) orapps/docs/content/docs/api.mdxapps/docs/content/docs/meta.json- optional docs config/util files for OpenAPI URL env handling
12. Build and Verification Commands
pnpm --filter @cio/api buildpnpm --filter @cio/api upload:openapipnpm --filter @cio/docs build- Manual check: docs navigation shows
API, open/api, and verify new endpoints appear.
Note: Follow AGENTS.md for code practices.