Lock core product decisions: attachment-level binding model with form_attachment table, create-in-context authoring, purchase-request pipeline replacing the hardcoded payment modal, per-lesson pulse feedback, landing-page form and reviews blocks, admin/teacher-only form creation, and free-standalone/paid-integrations commercial model. Defer respondent save-and-resume, edit-submitted-response, and one-question-per-page to phase 2. Add FORM-F1 to FORM-F9 tickets.
19 KiB
19 KiB
Forms Product Implementation Plan
Scope Lock
- Forms is a new top-level product domain and must not be implemented by repurposing course exercise routes or submission entities directly.
- Shared question rendering and answer modeling must be reused from
@cio/question-typesand@cio/ui/custom/exercise-question. - Exercise behavior must remain backward compatible throughout delivery.
- MVP includes public forms and authenticated organization forms.
- Invite-only / explicitly targeted forms are phase 2, not MVP.
- MVP includes draft, published, closed, and archived lifecycle states.
- MVP includes response inbox, per-question aggregates, and CSV export.
- MVP excludes grading, points, reviewer workflow, and correctness semantics.
- MVP may ship with form-specific question tables rather than a generalized cross-product questionnaire schema.
- Public forms must ship with abuse controls before general rollout.
- Binding is attachment-level:
form_attachmentbinds a reusable form to a context (course, lesson, landing page, checkout) and holds lifecycle/limit/dedup settings; responses record their attachment. - MVP includes the purchase-request pipeline (default org form replacing
payment-modal.svelte, pending → approve → enroll) and per-lesson pulse feedback. - MVP is one-page presentation only; respondent save-and-resume (FORM-B6) and edit-submitted-response are deferred to phase 2. Author-side DRAFT → PUBLISHED lifecycle stays.
- Commercial model is free-standalone / paid-integrations: free orgs get unlimited forms with share-link collection, inbox, analytics, and export; attachments, triggers, and consequences (approve→enroll, lesson pulse, landing-page blocks) require a paid plan. Checkout exception: the default purchase-request form collects on free; one-click approve→enroll is paid. Self-hosted ungated.
- Form creation and response access are restricted to org admins and teachers; students can only respond.
Delivery Phases
- Phase A: Shared foundations and schema.
- Phase B: API validation, services, and public/internal response flow.
- Phase C: Dashboard builder and management UI.
- Phase D: Response inbox, analytics, and export.
- Phase E: Hardening, abuse controls, rollout, and follow-up platform extraction.
Ticket Breakdown
| ID | Phase | Area | Task | Key Files | Depends On | Done When |
|---|---|---|---|---|---|---|
| FORM-A1 | A | Shared Contracts | Audit @cio/question-types and @cio/ui/custom/exercise-question for exercise-only assumptions and document the locked reusable surface |
packages/question-types/*, packages/ui/src/custom/exercise-question/*, prd/forms-product/README.md |
None | Reuse boundaries are explicit and no hidden coupling remains undiscovered |
| FORM-A2 | A | Shared Labels | Extend shared render contracts so host apps can inject forms-specific labels and submit copy without introducing hardcoded strings | packages/question-types/src/exercise-types.ts, packages/question-types/src/render-contract.ts, packages/question-types/src/index.ts |
FORM-A1 | Forms runner can use shared components with product-appropriate copy |
| FORM-A3 | A | DB Schema | Add form table with lifecycle, access, response settings, timing, slug, and org ownership |
packages/db/src/schema.ts |
None | Schema compiles and form entity supports draft/publish/close flows |
| FORM-A4 | A | DB Schema | Add formQuestion and formQuestionOption tables aligned to shared question contracts |
packages/db/src/schema.ts |
FORM-A3 | Form questions/options persist independently of exercises |
| FORM-A5 | A | DB Schema | Add formResponse and formResponseAnswer tables for respondent identity and answer storage |
packages/db/src/schema.ts |
FORM-A3, FORM-A4 | Responses can be stored without grading or groupmember coupling |
| FORM-A6 | A | DB Relations/Types | Add DB relations and exported inferred types for forms entities | packages/db/src/relations.ts, packages/db/src/types.ts |
FORM-A3, FORM-A4, FORM-A5 | Query layer can consume typed forms entities cleanly |
| FORM-A7 | A | DB Migration | Generate migration SQL and metadata for all new forms tables and constraints | packages/db/src/migrations/* |
FORM-A3, FORM-A4, FORM-A5 | Migration applies cleanly against local DB |
| FORM-A8 | A | Validation | Add form CRUD schemas, publish/close schemas, public response schemas, and response query schemas | packages/utils/src/validation/form/form.ts, packages/utils/src/validation/form/index.ts |
FORM-A3, FORM-A4, FORM-A5 | API can validate form authoring and response payloads consistently |
| FORM-A9 | A | Query Layer | Add CRUD queries for forms, form questions/options, and response storage/read paths | packages/db/src/queries/form/* |
FORM-A6 | Query layer supports form create/read/update/publish/respond flows |
| FORM-B1 | B | API Errors | Add forms-specific error codes for unavailable form, closed form, invalid responder, response limit reached, and duplicate response | apps/api/src/utils/errors.ts |
None | Forms routes can return precise failure codes |
| FORM-B2 | B | API Service | Add org-side forms service for create, get, list, update, duplicate, archive, publish, and close | apps/api/src/services/form/form.ts, apps/api/src/services/form/index.ts |
FORM-A8, FORM-A9 | Admin lifecycle actions work through a single service layer |
| FORM-B3 | B | API Service | Add public/internal form runner service to resolve form by slug and compute access rules | apps/api/src/services/form/runner.ts |
FORM-A8, FORM-A9 | Public and authenticated forms load through one normalized response model |
| FORM-B4 | B | API Service | Add response submission service with typed answer serialization using shared question contracts | apps/api/src/services/form/response.ts |
FORM-A8, FORM-A9 | Shared renderer payloads can be submitted and persisted end-to-end |
| FORM-B5 | B | API Service | Implement one-response-per-user/email and response-limit enforcement | apps/api/src/services/form/response.ts |
FORM-B4 | Duplicate/over-limit submissions are blocked server-side |
| FORM-B6 | B | API Service | Add authenticated draft save/resume support only if retained in MVP; otherwise defer and mark explicitly | apps/api/src/services/form/response.ts, apps/api/src/services/form/runner.ts |
FORM-B4 | Scope decision is resolved and API shape is stable |
| FORM-B7 | B | API Routes | Add organization management routes for forms CRUD and lifecycle actions | apps/api/src/routes/organization/form.ts, route exports, apps/api/src/app.ts |
FORM-B2 | Org admins can manage forms over typed RPC routes |
| FORM-B8 | B | API Routes | Add public routes for GET /forms/:slug and POST /forms/:slug/respond |
apps/api/src/routes/public/form.ts, apps/api/src/app.ts |
FORM-B3, FORM-B4, FORM-B5 | Public runner is reachable without course context |
| FORM-B9 | B | API Routes | Add org response routes for list/detail/export preparation | apps/api/src/routes/organization/form.ts |
FORM-B2, FORM-B4 | Dashboard can load responses and analytics data |
| FORM-B10 | B | File Upload | Reuse upload answer payload model for forms file upload and add size/type restrictions by access mode | apps/api/src/services/form/response.ts, shared upload helpers, form routes |
FORM-B4 | File-upload questions work safely for forms |
| FORM-C1 | C | Dashboard Types | Add forms feature request/response types inferred from API contracts | apps/dashboard/src/lib/features/forms/utils/types.ts |
FORM-B7, FORM-B8, FORM-B9 | Dashboard code uses inferred API types only |
| FORM-C2 | C | Dashboard API | Add forms API classes for management, runner, and responses | apps/dashboard/src/lib/features/forms/api/*.svelte.ts, .server.ts where needed |
FORM-C1 | Components can call form APIs through feature-local classes |
| FORM-C3 | C | Navigation | Add forms entry point and routing in dashboard navigation | apps/dashboard/src/lib/features/ui/navigation/*, new routes under apps/dashboard/src/routes/forms/* |
FORM-C2 | Admin can discover and enter forms product from dashboard |
| FORM-C4 | C | Builder State | Create forms builder store/state using shared question contracts without course/exercise dependencies | apps/dashboard/src/lib/features/forms/store/*, util files |
FORM-C2 | Builder state supports question add/edit/reorder/preview |
| FORM-C5 | C | Builder UI | Build forms editor page reusing shared question renderer/edit mode patterns | apps/dashboard/src/lib/features/forms/components/builder/*, routes |
FORM-C4 | Admin can author a form end-to-end |
| FORM-C6 | C | Settings UI | Build form settings panel for lifecycle, access mode, identity collection, limits, and success screen copy | apps/dashboard/src/lib/features/forms/components/settings/* |
FORM-C5 | Admin can configure MVP settings without raw JSON |
| FORM-C7 | C | Public Runner UI | Build form runner page for public and authenticated response flows using shared question renderer in take mode |
apps/dashboard/src/routes/forms/respond/[slug]/+page.svelte or public site equivalent, feature components |
FORM-C2, FORM-B8 | Respondents can complete and submit forms |
| FORM-C8 | C | Preview UI | Add builder preview mode matching shared runner behavior | apps/dashboard/src/lib/features/forms/components/preview/* |
FORM-C5, FORM-C7 | Admin preview matches live runner closely |
| FORM-C9 | C | Templates UI | Add starter templates for feedback, review, enrollment, intake, RSVP, and staff request | apps/dashboard/src/lib/features/forms/components/templates/*, template seed/query source as needed |
FORM-C5 | Admin can create common forms quickly |
| FORM-C10 | C | Translations | Add forms translation keys and remove hardcoded UI copy from forms feature | apps/dashboard/src/lib/utils/translations/en.json and peer locales as needed |
FORM-C5, FORM-C6, FORM-C7 | Forms UI follows translation conventions |
| FORM-D1 | D | Response Inbox | Build responses list with filters, counts, completion metadata, and detail drill-in | apps/dashboard/src/lib/features/forms/components/responses/*, routes |
FORM-B9, FORM-C2 | Admin can inspect submissions without export |
| FORM-D2 | D | Response Detail | Build single-response review view using shared question replay patterns | apps/dashboard/src/lib/features/forms/components/responses/response-detail.svelte |
FORM-D1 | Admin can inspect each answer question-by-question |
| FORM-D3 | D | Analytics | Build per-question aggregate analytics using submission summary/chart patterns already used in exercises | apps/dashboard/src/lib/features/forms/components/analytics/* |
FORM-D1 | Admin can see distribution charts and basic trends |
| FORM-D4 | D | Export | Implement CSV export service and dashboard trigger | apps/api/src/services/form/export.ts, form routes, dashboard responses UI |
FORM-B9, FORM-D1 | Export produces one row per response with respondent metadata and question columns |
| FORM-D5 | D | Permissions | Ensure only authorized org users can view responses and exports | forms services/routes, auth middleware wiring | FORM-B7, FORM-B9 | Response data is not exposed to unauthorized users |
| FORM-E1 | E | Abuse Controls | Add rate limiting and bot protection for public response endpoints | public form routes, middleware/config | FORM-B8 | Public forms are protected against basic spam/abuse |
| FORM-E2 | E | Monitoring | Add logging and monitoring hooks for submit failures, file-upload abuse, and export usage | forms services/routes | FORM-B8, FORM-D4, FORM-E1 | Operational visibility exists before rollout |
| FORM-E3 | E | QA/Tests | Add validation, query, API, and dashboard tests for form create/publish/respond/export flows | tests across packages/utils, packages/db, apps/api, apps/dashboard |
FORM-D4 | Core flows have regression coverage |
| FORM-E4 | E | Rollout | Add feature flag, rollout checklist, and production enablement notes | forms config/docs, PRD references | FORM-E1, FORM-E2, FORM-E3 | Safe staged rollout is documented and executable |
| FORM-E5 | E | Follow-up Platforming | Evaluate whether to generalize forms/exercises into a shared questionnaire persistence layer after MVP stability | follow-up PRD/docs | FORM-E4 | Decision is made with real usage data instead of pre-optimizing |
| FORM-F1 | A | DB Schema | Add formAttachment table (context type, course/lesson refs, lifecycle window, response limit, dedup flag) and formResponse.formAttachmentId + approvedForDisplay |
packages/db/src/schema.ts, relations/types |
FORM-A3, FORM-A5 | Reusable forms can bind to contexts and responses resolve to their attachment |
| FORM-F2 | B | API Service | Attachment CRUD + response filtering by attachment/context; library view aggregates across attachments | apps/api/src/services/form/attachment.ts, form routes |
FORM-F1, FORM-B2 | Course context shows only its responses; library shows per-context breakdown |
| FORM-F3 | C | Attach Picker | "Attach a form" picker component with inline create-in-context (builder in modal/drawer, template pre-seeded by context) | apps/dashboard/src/lib/features/forms/components/attach-picker/*, course/lesson settings surfaces |
FORM-C5, FORM-F2 | Admin can attach or create+attach a form without leaving the course |
| FORM-F4 | B/C | Purchase Pipeline | Seed default "Course purchase request" form per org; checkout attachment on paid courses; submit stores PENDING response then redirects to paymentLink; preserve teacher/student emails |
seed logic, apps/api/src/services/form/purchase.ts, org-site course routes |
FORM-F2 | Buy click leads to the org form and a stored purchase request; legacy payment-modal.svelte retired |
| FORM-F5 | C/D | Approve → Enroll | Response inbox actions for purchase requests: approve (enrolls student), reject with reason | forms inbox components, enrollment service wiring | FORM-F4, FORM-D1 | Admin approval enrolls the student without leaving the inbox |
| FORM-F6 | C | Lesson Pulse | Per-lesson pulse feedback attachment shown after lesson completion, with per-lesson aggregate view for instructors | lesson completion surface, apps/dashboard/src/lib/features/forms/components/analytics/* |
FORM-F2, FORM-D3 | Instructor sees pulse results grouped by lesson |
| FORM-F7 | C | Landing Page Blocks | Landing-page form section block and approved-reviews display block (consent checkbox + approvedForDisplay curation) |
packages/ui landing themes, org landing-page editor |
FORM-F2, FORM-C7 | Org can embed a form and display curated reviews on its public site |
| FORM-F8 | E | Plan Gating | Gate the integration layer (attachment creation, approve→enroll, lesson pulse, landing-page blocks) behind paid plans; standalone forms stay ungated and unlimited; default checkout form collects on free with approve→enroll gated; self-hosted ungated | plan config, form/attachment services, dashboard upsell states | FORM-B2, FORM-F2, FORM-F4 | Free orgs can collect without limits; every integration touchpoint shows a paid upsell instead of failing silently |
| FORM-F9 | B/C | Permissions | Restrict form creation, editing, response viewing, and export to org ADMIN and TUTOR roles across routes and dashboard nav; students only respond | forms routes middleware, dashboard nav/route guards | FORM-B7, FORM-C3 | Student accounts cannot reach form authoring or response data |
Route Targets
Organization Management Routes
GET /organization/:orgId/formsPOST /organization/:orgId/formsGET /organization/:orgId/forms/:formIdPUT /organization/:orgId/forms/:formIdPOST /organization/:orgId/forms/:formId/publishPOST /organization/:orgId/forms/:formId/closeGET /organization/:orgId/forms/:formId/responsesGET /organization/:orgId/forms/:formId/responses/:responseIdGET /organization/:orgId/forms/:formId/export
Public / Runner Routes
GET /forms/:slugPOST /forms/:slug/respond
Optional Authenticated Draft Routes
Only if MVP keeps save/resume:
POST /forms/:slug/draftPUT /forms/:slug/draft/:draftId
Acceptance Criteria by Phase
Phase A
- New forms schema exists without altering exercise semantics.
- Shared question/render contracts are usable by forms without hardcoded exercise wording.
- Query layer can create and read forms, questions, and responses independently from courses.
- Build passes for
@cio/db,@cio/utils, and@cio/question-types.
Phase B
- Org admins can create, update, publish, close, and fetch forms through API routes.
- Public and authenticated forms can be loaded by slug without course membership.
- Response submission persists typed answers and enforces lifecycle and limit rules.
- File upload questions work with safe restrictions.
Phase C
- Dashboard has a discoverable Forms area.
- Admin can author a form, configure settings, preview it, and publish it.
- Respondent-facing runner works for public and authenticated forms.
- No hardcoded user-facing copy is introduced in dashboard forms UI.
Phase D
- Admin can list responses, open a single response, and inspect answers.
- Admin can see basic aggregate analytics for supported structured question types.
- CSV export includes response metadata and one column per question.
Phase E
- Public endpoints have rate limiting and bot mitigation.
- Submit, export, and failure paths are monitored.
- Automated tests cover create/publish/respond/export paths.
- Rollout can be staged safely behind a feature flag.
Verification Commands
pnpm --filter @cio/question-types buildpnpm --filter @cio/db buildpnpm --filter @cio/utils buildpnpm --filter @cio/api buildpnpm --filter @cio/dashboard build- Run targeted tests for forms validation, API routes, and dashboard feature flows.
Suggested First Sprint Slice
FORM-A1toFORM-A9,FORM-F1FORM-B1toFORM-B5,FORM-F2FORM-B7,FORM-B8FORM-C1,FORM-C2
This gets the core platform decision made early, lands the schema/query/API foundation, and proves the highest-risk assumption: that the shared question engine can power a new standalone product without breaking exercises.
Suggested Second Sprint Slice
FORM-C3toFORM-C8FORM-D1,FORM-D2FORM-E1FORM-F3toFORM-F5
This delivers an MVP loop: create form, publish form, submit response, inspect response, and keep public endpoints safe enough for controlled rollout — plus the flagship integration demo: attach a form from a course, and buy a paid course through the purchase-request pipeline with approve → enroll.
Post-MVP Follow-Up Candidates
- Invite-only / explicitly targeted forms.
- Save and resume drafts if not shipped in MVP.
- Edit submitted response.
- Multi-section/page builder if one-page MVP ships first.
- Shared questionnaire persistence model for exercises and forms.