This file defines execution standards for humans and coding agents working in this repository.
- Language: TypeScript everywhere.
- Runtime: Cloudflare Workers.
- Framework: Hono.
- Data: Postgres.
- Object storage: Cloudflare R2.
- Async jobs: DB-backed job messages in Postgres.
- UI: server-rendered HTML + htmx + Shoelace.
- React is out of scope for v1.
- Standards scope: Open Badges 3.0 only.
- Architecture policy: single-path implementation in v1. No dual runtimes, no parallel frameworks, no "either/or" code paths for the same capability.
If a proposed change conflicts with these guardrails, open an ADR before implementation.
Keep file ownership explicit so app.ts does not grow into a monolith again.
app.tsis the composition root only:- wire dependencies/factories
- register routes
- export the worker
- avoid embedding feature/business logic here
routes/: HTTP endpoint handlers and request/response shaping.auth/: tenant/session/permission guard helpers.badges/: badge issuance, public badge page/view models, revocation metadata, recipient identifier logic.credentials/: credential verification checks and proof verification logic.signing/: signing key material, signing registries, DID/JWKS docs, credential signing helpers.ob3/: Open Badges 3.0 OAuth/discovery/auth helpers and error response shaping.http/: generic transport concerns shared across routes (middleware, shared request helpers).queue/: queue payload parsing, processing orchestration, and schedule-trigger utilities.lti/,learner/,presentation/,notifications/: domain-focused helpers per capability.
Refactor rule of thumb:
- if new behavior needs more than a small helper in
app.ts, create/extend a feature module and inject it fromapp.tsrather than implementing inline.
Use a strict TypeScript config and keep it green at all times.
Required compiler behavior:
strict: truenoImplicitAny: trueexactOptionalPropertyTypes: truenoUncheckedIndexedAccess: truenoImplicitOverride: trueuseUnknownInCatchVariables: truenoFallthroughCasesInSwitch: true
Rules:
- Do not use
anyin application code. - Use
unknownat boundaries and narrow explicitly. - Keep domain types explicit and reusable.
- All exported functions must have explicit parameter and return types.
- Model untrusted input with runtime schemas and inferred TS types.
- Validate all external input using Zod:
- HTTP bodies
- query/path params
- queue messages
- webhooks
- migration/import payloads
- Treat schema as the source of truth for boundary types.
- ESLint must run with type-aware rules (
typescript-eslintstrict type-checked presets). - Prettier is the single formatter.
- Lint rule severity target: zero warnings and zero errors in CI.
Style defaults:
- 2-space indentation.
- Semicolons enabled.
- Trailing commas where valid.
- Single quotes in TypeScript unless escaping hurts readability.
- Keep lines readable (target 100 chars, split long chains/objects).
- No unused imports or variables.
- No floating promises.
Every change must pass:
pnpm lintpnpm typecheckpnpm test
Type safety is a release gate:
tsc --noEmitmust pass.
- Prefer server-rendered pages and HTML forms over client-heavy abstractions.
- Use htmx for partial updates instead of introducing SPA complexity.
- Keep client JavaScript minimal and local to the feature.
- Choose straightforward code over clever code.
- Implement one clear way to do each thing in v1; defer alternatives.
- All tenants use shared Postgres with strict tenant isolation in v1.
- Do not bypass tenant scoping in queries.
- Use idempotency keys for issuance and revocation operations.
Before submitting:
- Confirm architecture guardrails are unchanged.
- Confirm no
anywas introduced. - Confirm lint, typecheck, and tests pass.
- Confirm formatting is clean and consistent.
- Confirm no dead code, TODO noise, or debug logs remain.
- Architecture decisions:
docs/adr/ - This file: implementation discipline and quality standards