@airdraft/cloud
v0.1.18
Published
Airdraft Cloud — MongoDB schemas, GitHub App handlers (relay, callback, webhook)
Readme
@airdraft/cloud
Pure business-logic handlers for the Airdraft Cloud dashboard. All handlers are framework-agnostic async functions that accept a Request and return a Response — wire them into Next.js route handlers, Express routes, or any HTTP server.
Design principle: Zero framework coupling. The cloud app (
aevrHQ/airdraft-cloud) is a thin glue layer that imports these handlers and re-exports them from 5–10 line route files. All business logic lives here.
Installation
npm install @airdraft/cloud
# or
pnpm add @airdraft/cloudUsage
// app/api/v1/teams/[teamSlug]/invites/route.ts (Next.js example)
import { handleInviteMember } from '@airdraft/cloud'
import { cloudDb } from '@/lib/db'
import { authAdapter } from '@/lib/auth'
export async function POST(req: Request, ctx: { params: Promise<{ teamSlug: string }> }) {
const { teamSlug } = await ctx.params
return handleInviteMember(req, cloudDb, authAdapter, teamSlug)
}Setup
1. Create the DB instance
import { MongoClient } from 'mongodb'
import { createCloudDb } from '@airdraft/cloud'
const client = new MongoClient(process.env.MONGODB_URI!)
export const cloudDb = createCloudDb(client)createCloudDb returns a typed CloudDb with collection handles for teams, projects, memberships, invites, customDomains, embedTokens, plans, auditEvents.
2. Create the auth adapter
import { betterAuth } from 'better-auth'
import { createBetterAuthAdapter } from '@airdraft/cloud-auth/better-auth'
const auth = betterAuth({ /* ... */ })
export const authAdapter = createBetterAuthAdapter(auth)See @airdraft/cloud-auth for the full adapter interface.
API
Team management — team.ts
| Function | Route | Description |
|---|---|---|
| handleListTeams(req, db, auth) | GET /teams | Teams the caller belongs to |
| handleCreateTeam(req, db, auth) | POST /teams | Create a team — body: { name: string } |
| handleGetTeam(req, db, auth, teamSlug) | GET /teams/:slug | Team details |
| handleUpdateTeam(req, db, auth, teamSlug) | PATCH /teams/:slug | Update team name/settings |
Project management — project.ts
| Function | Route | Description |
|---|---|---|
| handleListProjects(req, db, auth, teamSlug) | GET /teams/:slug/projects | Projects in a team |
| handleCreateProject(req, db, auth, teamSlug) | POST /teams/:slug/projects | Create project — body: { name } |
| handleGetProject(req, db, auth, projectSlug) | GET /projects/:slug | Project details |
| handleGenerateApiKey(req, db, auth, projectSlug) | POST /projects/:slug/api-key | Rotate API key, returns plaintext once |
| handleUpdateProjectSettings(req, db, auth, projectSlug) | PATCH /projects/:slug | Update project settings |
Invite & membership — invite.ts
| Function | Route | Description |
|---|---|---|
| handleInviteMember(req, db, auth, teamSlug) | POST /teams/:slug/invites | Send invite — body: { email, role } |
| handleAcceptInvite(req, db, auth, token) | POST /invites/:token/accept | Accept invite (email must match session) |
| handleRevokeInvite(req, db, auth, teamSlug, token) | DELETE /teams/:slug/invites/:token | Revoke pending invite |
| handleRemoveMember(req, db, auth, teamSlug, userId) | DELETE /teams/:slug/members/:userId | Remove a member (hierarchy enforced) |
| handleUpdateMemberRole(req, db, auth, teamSlug, userId) | PATCH /teams/:slug/members/:userId | Change role — body: { role } |
| handleLeaveTeam(req, db, auth, teamSlug) | DELETE /teams/:slug/members/me | Leave a team (last owner blocked) |
Role hierarchy: owner > admin > editor > viewer
- Callers can only remove or reassign members at a strictly lower tier than themselves
- The last
ownerof a team cannot be removed or leave - The
ownerrole cannot be assigned viahandleUpdateMemberRole— ownership transfer is out of scope
Note: handleInviteMember creates the invite and returns the token — it does not send email. Sending email is the app layer's responsibility (clone the response, read the token, call sendEmail).
Billing — billing.ts
| Function | Description |
|---|---|
| handleGetPlans(req, db) | List active PlanDoc entries |
| handleUpgradePlan(req, db, auth, pay) | Create a @untools/pay checkout session |
| handleWebhookSettlement(req, provider, db, pay) | Verify provider webhook + update TeamDoc.planSlug |
| handleGetUsage(req, db, auth, teamSlug) | Current usage vs plan limits |
| checkLimit(db, teamId, key) | Resolve plan (Redis-cached) → { allowed, current, max } |
| checkFeature(db, teamId, key) | Return a feature flag/value from the active plan |
Cloud CMS runtime — runtime.ts
import { createCloudCmsHandler } from '@airdraft/cloud'
const { GET, POST, PUT, PATCH, DELETE } = createCloudCmsHandler({
db: cloudDb,
schemaCache: tokenCache, // TokenCache (Redis or any get/set/del)
githubAppId: process.env.GITHUB_APP_ID!,
githubAppPrivateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
})
// Mount at: /cms/[projectSlug]/api/cms/[...route]
export { GET, POST, PUT, PATCH, DELETE }Handles per-project CMS requests — resolves the project from the slug, reads airdraft.schema.json from GitHub (Redis-cached), builds a CmsEngine, and delegates the request. Auth is resolved via project API key or team membership JWT.
Use bustEngineCache(projectId) from a webhook handler to invalidate the in-memory engine cache on push events.
GitHub — relay.ts, callback.ts, webhook.ts
| Function | Description |
|---|---|
| handleTokenRelay(req, db, opts) | Relay GitHub installation tokens to dashboard clients |
| handleCallback(req, opts) | Process GitHub App OAuth callback + store installation ID |
| handleWebhook(req, opts) | Handle GitHub webhook events (push → bust schema cache) |
State / JWT utilities — state.ts, jwt.ts, cli.ts
| Function | Description |
|---|---|
| signState(payload, secret) | Sign CSRF state token for OAuth flows |
| verifyState(token, secret) | Verify + decode CSRF state token |
| signGitHubAppJwt(appId, privateKey) | Sign a GitHub App JWT for API calls |
| signCliToken(userId, secret) | Sign a 15-min CLI JWT |
| verifyCliToken(token, secret) | Verify CLI JWT → userId or null |
Error response shape
All handlers return errors in the format used by AirdraftClient:
{ "error": { "code": "FORBIDDEN", "message": "Requires owner or admin role" } }Common error codes: UNAUTHORIZED (401), FORBIDDEN (403), NOT_FOUND (404), CONFLICT (409), LAST_OWNER (403), EMAIL_MISMATCH (403), INVITE_EXPIRED (410).
Types
import type {
CloudDb, TeamDoc, ProjectDoc, MembershipDoc, InviteDoc,
PlanDoc, AuditEventDoc, TokenCache,
} from '@airdraft/cloud'Changelog
See CHANGELOG.md.
