Below is a developer-ready specification you can hand straight to GitHub Copilot (or paste into a TECH_SPEC.md) and begin scaffolding.
It extends the dynamic-template idea I shared earlier, but now enumerates every service you listed, every project phase, every form/document, the DB schema, all key API routes, and the React component tree—plus some UX notes so the build stays on-brand.
| Abbr | Meaning |
|---|---|
| ST | Service Type (Screen Print, SaaS Dev, etc.) |
| PL | Phase Library entry (ideation, review, etc.) |
| FM | Form Module (JSON-schema rendered as UI) |
| DM | Document Module (Handlebars/MD → PDF/HTML) |
| PD | Project Definition (per-project merged JSON) |
| UID | React component that renders a module |
┌── code ─────────────────────────────────────────────────────────┐
| id | code | Display Name | Default Phase Keys |
|----+------+----------------------+-----------------------------|
| 1 | COL | Collaboration Only | ONB → COLLAB → WRAP |
| 2 | IDE | Ideation Workshop | ONB → IDEA → WRAP |
| 3 | SP | Screen Printing | ONB → IDEA → PREP → PRINT → LAUNCH|
| 4 | LFP | Large-Format Print | ONB → PREP → PRINT → LAUNCH |
| 5 | GD | Graphic Design | ONB → IDEA → DSGN → REV → LAUNCH|
| 6 | WW | Woodworking | ONB → IDEA → CAD → FAB → FINISH → LAUNCH|
| 7 | SAAS | SaaS Development | ONB → DISC → MVP → QA → DEPLOY → LAUNCH|
| 8 | WEB | Website Design | ONB → DISC → DSGN → DEV → REV → DEPLOY → LAUNCH|
| 9 | BOOK | Book Cover Design | ONB → IDEA → DSGN → REV → LAUNCH|
|10 | LOGO | Logo & Brand System | ONB → RESEARCH → DSGN → REV → LAUNCH|
|11 | PY | Python Automation | ONB → DISC → DEV → QA → LAUNCH|
└────────────────────────────────────────────────────────────────┘Every code above is referenced throughout forms, docs, permissions, etc.
interface Phase {
key: "ONB" | "IDEA" | "DISC" | ...;
label: string; // “Onboarding”
icon: string; // lucide icon id
uids: string[]; // React UIDs to show in portal
formModules: string[]; // e.g. ["intake_base","intake_sp"]
docModules: string[]; // e.g. ["proposal_default"]
permissions: RolePerms; // CRUD rules per role
}| Key | Purpose | Core UID(s) |
|---|---|---|
| ONB | Kick-off / intake forms | <IntakeWizard /> |
| COLLAB | Light collab (brain-dump board) | <MiroEmbed /> |
| IDEA | Ideation & mood-board | <Moodboard /> |
| RESEARCH | Brand/market research dossier | <ResearchList /> |
| DISC | Discovery workshop doc capture | <NotionEmbed /> (opt) |
| DSGN | Design production phase (Figma embeds) | <FigmaPreview /> |
| CAD | 3-D/CAM preview (STL viewer) | <ModelViewer /> |
| PREP | Pre-Press checklist + proof approval | <ProofChecklist /> |
| Print queue + batch tracker | <BatchStatus /> |
|
| MVP | First working slice for SaaS | <StagingLink /> |
| DEV | Web build commits (Netlify previews) | <DeployCard /> |
| QA | Bug tracker embed | <LinearList /> |
| FAB | Woodshop fabrication log | <FabricationLog /> |
| FINISH | Sand / stain / lacquer stage | <FinishChecklist /> |
| DEPLOY | Prod deploy checklist | <LaunchChecklist /> |
| REV | Formal review & feedback cycle | <AnnotationBoard /> |
| LAUNCH | new name replacing “Delivery”: asset hand-off & celebration | <LaunchGallery /> |
| WRAP | Post-mortem, payment, testimonial | <WrapUp /> |
| Module ID | Target Phase(s) | Service Filters | Why it exists |
|---|---|---|---|
intake_base.json |
ONB | all | contact, billing, goals |
intake_sp.json |
ONB | SP,LFP | garment, ink colors, qty |
intake_ww.json |
ONB | WW | wood type, dims, finish |
intake_saasi.json |
ONB | SAAS,WEB,PY | repo URLs, domain ideas |
proposal_default.hbs |
IDEA,DISC | all | auto-pricing table |
sow_print.hbs |
PREP | SP,LFP | spoilage + Pantone clause |
proof_approval.json |
PREP,REV | GD,SP,LFP | checkbox: “spelling correct” |
bug_report.json |
QA,REV | SAAS,WEB,PY | severity, repro-steps |
deploy_checklist.md |
DEPLOY | SAAS,WEB | env vars, backups |
launch_certificate.hbs |
LAUNCH | all | “Project successfully launched” |
final_invoice.hbs |
WRAP | all | pulled from invoices table |
| (+ 15 more) | … | … | … |
Stored in /templates/forms & /templates/docs, version-tagged.
-- users
id uuid PK
email text unique
hash text
role enum('admin','client')
display_name text
twofa_secret text null
created_at timestamptz default now()
-- clients (orgs you bill)
id uuid PK
display_name text
contact_user uuid FK users(id)
-- projects
id uuid PK
client_id uuid FK clients(id)
name text
services text[] -- ['SP','GD']
phase_def jsonb -- stored PD
status enum('active','archived')
created_at timestamptz
-- phases (flattened for quick lookup)
id uuid PK
project_id uuid
key text -- 'PREP'
position int
status enum('not_started','in_progress','done')
started_at timestamptz
done_at timestamptz
-- forms_data
id uuid PK
phase_id uuid
module text
payload jsonb
submitted_by uuid FK users(id)
submitted_at timestamptz
-- docs (rendered PDFs, proposals, etc.)
id uuid PK
phase_id uuid
module text
file_url text
version smallint
generated_at timestamptz
-- invoices
id uuid PK
project_id uuid
seq int -- human-friendly #
issued_at date
due_at date
status enum('draft','sent','paid','overdue')
currency char(3) default 'USD'
total numeric(10,2)
-- invoice_items
invoice_id uuid
label text
qty int
rate numeric(10,2)
-- files (deliverables & refs)
id uuid PK
project_id uuid
phase_id uuid
label text
url text
mime text
size int
uploaded_by uuidWhy separate
clientsfromusers? A company may have multiple user logins later (accounting vs creative team).
| Verb + Route | Auth | Body / Params | Resp |
|---|---|---|---|
POST /auth/login |
— | email, pwd | JWT |
POST /auth/2fa |
token | code | 200 / 401 |
GET /projects |
token | — | list |
POST /projects ‡ |
admin | name, clientId, services[] | PD json |
PATCH /projects/:id/status |
admin | status | 204 |
GET /projects/:id/phases |
token | — | ordered array |
POST /phases/:phaseId/forms |
token | moduleId, payload | 201 |
POST /files |
token | phaseId + file (multipart) | file meta |
POST /invoices ‡ |
admin | projectId, items[] | invoiceId |
GET /invoices/:id |
token | — | invoice data |
POST /payments/stripe-hook |
∅ | Stripe webhook | 200 |
‡ Admin-only routes hidden in Admin UI.
_app
├─ Layout
│ ├─ SiteHeader (public)
│ ├─ Footer
│ └─ PortalHeader (+BrandedLine)
├─ pages/
│ ├─ index (Landing)
│ ├─ inquiry
│ ├─ login
│ └─ portal/
│ ├─ dashboard
│ ├─ projects/[id]/
│ │ ├─ index (tabs)
│ │ ├─ phases/[phaseKey].tsx ← dynamic renderer
│ │ └─ invoices
│ └─ admin/…
└─ components/
├─ BrandedLine.tsx
├─ PhaseStepper.tsx
├─ FormRenderer.tsx (jsonschema-to-formik)
├─ FileCard.tsx
├─ InvoiceCard.tsx
├─ PaymentButton.tsx
└─ …per-phase UIDs
Phase pages call /api/projects/:id/phases → decide which FormRenderer / UID grid to show.
- ONB – fills intake_base + any ST-specific forms
- IDEA – drops Pinterest links; you upload proposal.pdf → auto-notify
- DSGN – views live Figma embed; clicks Approve → phase auto-advances
- PREP/PRINT – reviews proof checklist; marks ✓; status flips to READY TO PRINT
- LAUNCH – downloads asset bundle ZIP; sees Launch Certificate
- WRAP – invoice list shows Invoice #3 Pending → Stripe pay → success receipt
Admin portal shows Kanban-board of all active phases across projects (column per phase key). Drag card to move or click to open phase modal (same UI as client but with edit toggle).
function buildCompositeSchema(phase, project) {
const mods = phase.formModules
.filter(mId => serviceFilterOk(mId, project.services));
const merged = mods.reduce(mergeJSONSchema, baseSchema());
return merged;
}
GET /forms/:phaseId => {schema, ui}Client receives above, renders instantly; no redeploy needed when you add a new service.
- JWT Bearer tokens +
helmet()middleware - RBAC middleware
can(user, action, resource)on every route - Signed URLs (S3 pre-signed) for private file downloads
- Stripe webhooks verify signature secret before mutating invoice status
- CSP headers to limit embeds (Figma, Pinterest, Linear, etc.)
- Automatic nightly DB dump to encrypted S3 bucket
-
GitHub Actions
- Lint, type-check, Jest
- Cypress smoke-run (login, project list, invoice pay test in Stripe test-mode)
- Deploy to Vercel with preview URL → comment [PREVIEW] on PR
(next route /pdf/:docId) (Chromium headless)
↓ ↓
Render React doc page → Puppeteer → save PDF → S3
Because docs share React component tree, they keep branding + fonts 100 % identical between HTML & PDF.
| Service Type | Extra Form Modules | Extra Docs (outside defaults) |
|---|---|---|
| COL | collab_brief.json |
— |
| IDE | ideation_goals.json |
mural_export.md |
| SP,LFP | intake_sp.json, proof_approval.json |
print_spec_sheet.hbs, ink_mix_chart.pdf |
| GD,BOOK,LOGO | design_brief.json |
brand_guidelines.hbs |
| WW | intake_ww.json, cad_settings.json |
cutlist.pdf |
| SAAS,WEB,PY | intake_saasi.json, feature_scope.json |
api_contract.md, deploy_checklist.md |
| BRAND | brand_audit.json |
identity_system.pdf |
(If a project combines services, union the lists—duplicates auto-deduped).
Spin up npx create-next-app reprint-portal, copy your existing styles.css palette, scaffold the tables with Prisma or Knex, then let Copilot implement one module at a time.
Everything above is modular—add a new offering tomorrow by writing one FM JSON and updating the Service Type matrix. The portal instantly adapts, keeping your minimalist Virgil-Abloh-meets-West-Side flair intact.