🧭 Product & Domain Overview

πŸ“‹ Summary

Midshift is a web application that runs the production side of retail advertising campaigns: it takes a campaign from brief and media plan through copywriting, artwork, video edits, tracking links and approvals to a final "handover" grid that a client, media buyer or archive team can take away. Its central idea is that every advert ("ad unit") has one agreed name, and that name is used to join the copywriter's Word document, the designer's exported files and the app's own records, so files and copy land in the right place without manual sorting. The code shows a clear, opinionated product with a well-defined audience (an in-house retail marketing team plus its agencies and suppliers) and several genuinely distinctive features, notably a forgiving copy-deck parser and a shared filename-to-advert matcher. It is built to be installed once per client (one database and one hosting project each), not as a shared multi-customer service, and it has no translation layer: the interface is English only. Market position, pricing, customers and revenue cannot be judged from the repository.

🎯 Rating

🟒 ●●●●○ 4 / 5 β€” Good (Green). Purpose, domain model and user journeys are coherent and consistently expressed in code, schema, tests and UI copy; the score is held back from 5 by scope sprawl (two navigation shells, orphaned and removed features still present on disk, four campaign "kinds") and small schema/type mismatches in the role model.

πŸ” Findings

1. Purpose and problems solved

ProblemHow the product addresses itEvidence
Copy arrives in free-form Word documents and is re-typed into ad tools.docx/text "copy deck" import that auto-detects structured and free-form layouts, creates or updates ad units, carousel cards and Uber "Journey Ad" states, and shows a word-level diff before committing[Observed] src/lib/copy-deck-parser.ts:824 (parseCopyDeck, 828 lines); src/app/api/campaigns/[id]/import-copy/route.ts; src/lib/word-diff.ts; baseline regression test src/lib/copy-deck-parser.baseline.test.ts:14-16 (22 units, 0 warnings)
Supplier and designer files arrive with arbitrary namesOne filename→unit matcher shared by every upload path, plus an "Asset names" manifest so suppliers name files correctly in advance[Observed] src/lib/asset-match.ts (374 lines, 98% line coverage per shared facts); asset-manifest-modal.tsx referenced in CLAUDE.md
Approvals go stale silently when copy changesApproved copy that changes (by edit or re-import) is reset to pending_review and a new "copy round" opens[Observed] src/lib/approval-events.ts; documented behaviour of PATCH /api/campaigns/[id]/ad-units/[unitId] in docs/API.md:89
Tracking (UTM) links built inconsistentlyAccount-level UTM template with tokens; one builder buildUtm[Observed] src/lib/utm-template.ts:133 (100% coverage)
"Who is waiting on whom" is unclear across agency and clientDerived per-person worklist ("For you"), notifications, overdue sweep cron, briefs-waiting queue[Observed] src/lib/for-you.ts (291 lines), /api/for-you, /api/cron/overdue, /api/briefs/waiting
Large video files and Photoshop files are awkward to reviewBrowser-side poster-frame capture for video and PSD→PNG flattening; only the derived image is uploaded[Observed] src/lib/video-poster.ts, src/lib/psd-png.ts (ag-psd dependency)
Proof of custody at archiveFinal grid, share link, "DAM ingest" confirmation and a retention purge that waits for both archive and ingest[Observed] src/lib/retention.ts, /api/campaigns/[id]/dam-archive, /api/cron/purge-review

The README's own framing is "the record that makes a sign-off mean something" and "nothing is stored as 'ready', 'approved' or 'live' that the artefacts don't prove" ([Observed] README.md:3-12). ARCHITECTURE.md states the invariant "Status is derived, never stored" and points to src/lib/campaign-derive.ts, src/lib/final-load.ts and src/lib/for-you.ts ([Observed] ARCHITECTURE.md:25-33). Those files exist and implement derivation on read.

2. Business domain and target users

Domain [Observed]: retail marketing campaign production across paid social, display, programmatic, ride-hailing and out-of-home. The channel enum is meta, tiktok, google, pinterest, triplelift, uber, ooh (supabase/baseline.sql:60). Campaigns have a kind of season | concept | rollout | ooh (supabase/baseline.sql:2091): a season groups jobs, a concept job holds the big idea and master video, rollouts adapt it to channels, and OOH jobs manage billboard/LED sites.

Geography [Inferred]: deployment and compliance documents analyse South African data-protection law (POPIA) and Vercel's Cape Town region (docs/NEW_DEPLOYMENT.md:110-127; design/COMPLIANCE.md:12-14), indicating a South African launch market. Not confirmable from code.

Users and roles [Observed]:

LayerValuesSource
Global tier (TypeScript)admin | editor | reviewersrc/types/database.ts:3
Global tier (database enum)designer, copywriter, brand_manager, media_planner, account_manager, admin, super_admin, member, editor β€” no reviewersupabase/baseline.sql:46-49
Per-campaign tierviewer | editor | adminsupabase/baseline.sql:104
Discipline (what part of the job is yours)brand manager, project manager, traffic, copywriter, proofreader, art director, designer, video producer, media buyersrc/lib/discipline.ts:19-22
External, no accountSupplier (token upload page /upload/[token]), client / archive partner (token share page /preview/[token])src/app/upload/[token]/, src/app/preview/[token]/

The role model is coherent in intent (a tier says whether you may write; a discipline says which part), but the global tier is inconsistent between code and schema: the app accepts reviewer as a valid role in the invite and user-edit routes (src/app/api/admin/invite/route.ts:7, src/app/api/admin/users/[id]/route.ts:10) and gates several screens on it (e.g. units-page.tsx:172), but no migration adds reviewer to the user_role enum (searched all alter type ... add value statements). [Inferred] Assigning the reviewer role would be rejected by Postgres, so the "reviewer" experience the UI codes for is likely unreachable. The legacy enum values left by migrations 031–034 are also still present.

3. Differentiators evident from the implementation

DifferentiatorAssessmentEvidence
Copy-deck parser for messy real-world Word decks (section banners, bare-name units, colon headers, carousel card markers, placeholder stripping, audience cleaning)Most distinctive IP; well tested (85% line coverage; baseline fixture e2e/fixtures/aw26-deck.txt)[Observed] src/lib/copy-deck-parser.ts; rules documented in CLAUDE.md "The copy-deck parser"
Naming convention as join key + shared asset matcher + expected-filename manifestSimple idea, consistently enforced in every upload path[Observed] src/lib/asset-match.ts; buildName in src/lib/plan-change.ts; docs/NAMING_CONVENTION.txt
Rule-based "Ask" command bar (navigate to a section, open a unit, "what needs me"), with browser voice input; records only the class of question, never the sentencePrivacy-conscious and cheap to run; not an AI model β€” no LLM provider is called[Observed] src/lib/ask-intent.ts:1-40; src/app/api/ask/route.ts; voice via Web Speech API at src/app/(app)/workspace/ask-sheet.tsx:130-131,159
White-labelling: organisation name, logo and website are data set by an admin; product name from one env var; a "scrub" gate blocks client-identifying strings from the repositoryReal and enforced in CI[Observed] src/lib/branding.ts:14-19,168; src/lib/config.ts:13; scripts/scrub.js; CI step in .github/workflows/ci.yml
Discipline-scoped field permissions (a copywriter may change copy but not the link; per-field refusal)Unusual granularity for this category[Observed] canEditArea / canApprove in src/lib/api-auth.ts:174,193; 23 API route files use them
Retention tied to custody (files kept until both archived and confirmed ingested into the client's DAM, then 30 days)Thoughtful governance feature[Observed] src/lib/retention.ts; ARCHITECTURE.md Β§9
Browser-side media processing (video poster frames, PSD flattening, artwork fit/render)Avoids server compute and upload size limits[Observed] src/lib/video-poster.ts, src/lib/psd-png.ts, src/lib/artwork/

Competitive differentiation versus established tools (e.g. creative-ops, DAM or ad-ops platforms) is [Unavailable] β€” requires market analysis and customer interviews.

4. Strategic objectives implied by the code

ObjectiveWhat the code impliesTag
Single-tenant, per-client deploymentsNo tenant/organisation key on any table (searched supabase/baseline.sql for tenant, org_id, organisation_id: only a comment at line 1729). Organisation identity is a singleton in settings (org_name, org_logo_path). ARCHITECTURE.md Β§2 and docs/NEW_DEPLOYMENT.md Β§1 state "one deployment per client … never a fork".[Observed]
Multi-tenancy (SaaS)Not supported. Converting would require a tenant column on ~44 tables, RLS rewrite and per-tenant settings/branding.[Inferred]
White-labellingAchieved (see above). Retired env vars (NEXT_PUBLIC_CLIENT_NAME etc.) are no longer read (grep returns no hits in src).[Observed]
InternationalisationNone. No i18n library in package.json; UI strings are hard-coded English throughout components; <html lang="en"> (src/app/layout.tsx:52). Date/number formatting mixes locales: toLocaleDateString('en-GB') (debrief-doc.tsx:55), toLocaleString('en-US') (build-view.tsx:128), with a comment in src/lib/utils.ts:33 recording a past en-AU formatter. No currency handling found.[Observed]
Scale readinessDesigned for one organisation's team per deployment. Only one .range( pagination call and 16 .limit( calls in non-harness code; most lists load whole sets. Adequate for a single brand's campaigns; unproven for large archives.[Observed] counts; capacity [Unavailable] β€” no load data
Cost-light runtimeNo AI provider, no error tracker, no analytics; Vercel + Supabase + Resend only (README.md Architecture; ARCHITECTURE.md Β§2).[Observed]
Product direction"Product direction … recorded outside this repository" (docs/README.md:31-33). Next build target named as cropping/export on the ad unit's Artwork segment (CLAUDE.md Parked decisions).[Unavailable] beyond these notes

5. Core user journeys

Navigation: signed-in users are redirected from / to the workspace shell (/workspace); the older "classic" dashboard survives at /?classic=1 and old /campaign/[id]/… routes 308-redirect into the workspace (next.config.ts:23-33).

#JourneyWhoScreens (routes)Key API routes
1Sign in / accept invite / reset passwordAll/login, /invite/[token], /accept-invite, /auth/reset-password/api/invite/[token]/accept, /api/auth/forgot-password
2Set up the organisation (name, logo, business units, campaign types, ad-format library, UTM template, users)Admin/workspace/admin, /workspace/admin/activity/api/admin/* (organisation, business-units, categories, campaign-types, ad-formats, utm-template, invite, users)
3Create a season / concept / rollout / OOH jobAdmin, brand manager"What is this?" sheet in src/app/(app)/workspace/ws-new-campaign.tsx:359; /workspace/campaigns/api/campaigns/[id]/jobs, /api/campaigns/[id]/duplicate
4Write and submit a brief; traffic/PM picks it upBrand manager β†’ traffic/PM/workspace/campaign/[id]/brief, Home (/workspace) "Briefs waiting"/api/campaigns/[id]/brief, /api/briefs/waiting, /api/briefs/waiting/[versionId]/take
5Build the media plan, which creates named ad unitsTraffic / media buyer/workspace/campaign/[id]/plan, /dates/api/campaigns/[id]/plan, /api/campaigns/[id]/dates
6Concept & toolkit (big idea, shoot references, logos)Art director, designer/workspace/campaign/[id]/concept, /toolkit/api/campaigns/[id]/files, /files/upload-url
7Import copy deck, edit copy per unit/card, comment, approveCopywriter, proofreader, brand manager/workspace/campaign/[id]/ads (grid, tiles, unit panel)/api/campaigns/[id]/import-copy, /ad-units/[unitId], /frames, /approve, /cap, /api/ad-units/[id]/comments
8Drop artwork; files auto-match to unitsDesigner/workspace/campaign/[id]/ads/api/campaigns/[id]/upload-previews, /upload-asset, /render-push
9Video edits: versioned review, timecoded comments, supplier upload via linkVideo producer, supplier (no login)/workspace/campaign/[id]/production, /upload/[token]/api/campaigns/[id]/review/*, /upload-links, /api/upload/[token]/*, /api/review/*
10Build tracking linksMedia buyer/workspace/campaign/[id]/utms/api/admin/utm-template, ad-unit PATCH
11OOH site planTraffic/workspace/campaign/[id]/plan on an ooh job/api/campaigns/[id]/sites, /sites/[unitId], /sites/suggest
12Final grid, share link, archive and DAM-ingest confirmationTraffic, client/archive partner (token)/workspace/campaign/[id]/handover, /preview/[token]/api/campaigns/[id]/share-link, /dam-archive, /api/preview/[token]/custody
13Season wrap-up and carry recommendations forwardBrand manager/workspace/campaign/[id]/wrapup (+ season strategy)/api/campaigns/[id]/wrapup, /questions, /media-strategy
14Oversight: job history, notifications, feedback, AskAll / admin/workspace/campaign/[id]/history, /workspace/notifications, /workspace/help, Ask sheet/api/notifications, /api/feedback, /api/track, /api/ask, /api/for-you

The help page's section titles mirror this flow exactly ("How a job flows", "One name for everything", "The media plan makes the ad units", "The copy deck", "Files match themselves", "Video edits and the supplier link", "Who does what", "UTMs and the Final grid", "Out-of-home") ([Observed] src/app/(app)/workspace/help/help-view.tsx), which is good evidence that the product's mental model is shared between code and user-facing copy.

6. Coherence and scope observations

ObservationTag
Two shells co-exist: the "classic" /campaign/[id]/… pages (now redirected) and the workspace. Pages for removed features (checklist, build, assets, members under src/app/(app)/campaign/[id]/) and e2e/checklist.spec.ts remain on disk, described in CLAUDE.md as "unreachable and to be deleted".[Observed]
An orphaned brief/review/sign-off pipeline (briefs, review_rounds, /timeline, /debrief) is kept "deliberately as groundwork".[Observed] CLAUDE.md Parked decisions; src/app/timeline/ exists
Features cut or moved out of the repository: Studio (to a separate repository on 2026-09-19), Present wall, Figma plugin, session replay. Their schema remnants (studio_crits, studio_crit_comments, campaign_share_links.audience_id, is_creative_approver) stay.[Observed] CLAUDE.md; supabase/baseline.sql table list
High rate of product change: CLAUDE.md carries 61 dated entries between August and 2026-09-28, many recording reversals. Indicates an evolving, owner-led product rather than a settled one.[Observed] `grep -oE '20[0-9]{2}-…' CLAUDE.md
Fixture "story" (src/app/qa/fixtures.ts:4-27) describes one live campaign with 14 ad units across seven channels in every state; it is a clear, invented, executable description of the domain.[Observed]

7. Market positioning and commercial facts

ItemStatus
Customers, contracts, revenue, pricing[Unavailable] β€” requires commercial records
Number of live deployments[Unavailable]; docs refer to "the live deployment" and "the trial deployment" in the singular (CLAUDE.md, ARCHITECTURE.md:97), suggesting [Inferred] one live client
Team size[Inferred] one developer: "the repo has one developer" (ARCHITECTURE.md Β§8); git history squashed to a single commit (3789df6) so not verifiable
OwnershipProprietary, "Matthew Prins, trading as Midshift" (LICENSE)

🧾 Evidence

  • Files read: README.md, ARCHITECTURE.md (Β§1–3, 5, 8–9), CLAUDE.md, docs/NEW_DEPLOYMENT.md, docs/README.md, src/app/qa/fixtures.ts:1-30, src/lib/ask-intent.ts:1-40, src/lib/discipline.ts:19-32, src/lib/config.ts, src/lib/branding.ts, src/app/(app)/workspace/ask-sheet.tsx, src/app/(app)/workspace/ws-new-campaign.tsx, next.config.ts:23-33, supabase/baseline.sql (enums, tables).
  • find src/app -name page.tsx | grep -v '/qa/'β†’ 39 product pages (including the root/and the unreachable classic pages); the/qa/*` harness adds 24 more.
  • find src/app/api -name route.ts β†’ 83 route files.
  • grep -rln "i18n\|next-intl\|useTranslation" src and package.json β†’ no i18n library.
  • grep -rhoE "toLocale[A-Za-z]*\('[^']*'" src β†’ en-US Γ—1, en-GB Γ—1, en-AU Γ—1 (comment).
  • grep -n "create type user_role" -A3 supabase/baseline.sql β†’ enum without reviewer; grep -rn "add value" supabase/migrations β†’ no migration adds it.
  • grep -oiE 'create table …' supabase/baseline.sql | sort -u | wc -l β†’ 44 tables.
  • grep -rn "\.range(" src | grep -v qa/ | wc -l β†’ 1; .limit( β†’ 16.
  • Shared facts from the brief: test coverage figures, cloc, single commit.

⚠️ Risks

#RiskSeverityEffort
R1Single-tenant architecture limits a SaaS growth thesis: every new client is a new Vercel + Supabase project, manual SQL migrations per database, and per-deployment configuration. Operational cost grows linearly with clients.🟠 High (for a scale/SaaS thesis); Low (for a services/managed-deployment thesis)L to convert
R2No internationalisation: English-only hard-coded strings and mixed locale formatting; entering non-English markets requires an extraction pass over ~465 source files.🟑 MediumL
R3Role model mismatch: reviewer is a valid role in TypeScript and API validation but not in the database enum; the reviewer experience is likely unreachable and an invite with that role likely fails.🟑 MediumS
R4Scope sprawl and residue: unreachable pages, orphaned tables and parked pipelines increase onboarding cost and the chance of building on dead paths.🟑 MediumM
R5Product knowledge held outside the repository (product direction, open decisions on an external "comment ledger", data-protection posture "available on request").🟑 MediumS to M (obtain and file)
R6Concentration on one market's assumptions (South African POPIA framing, channel set incl. Uber Journey Ads and TripleLift) β€” [Inferred]; portability to other markets unverified.βšͺ LowM

βœ… Recommendations

#RecommendationPriorityEffort
1Decide the commercial model (managed per-client deployments vs multi-tenant SaaS) before valuation; if SaaS, scope tenant-key retrofit and automated migrations as a funded workstream.🟠 HighS (decision) / L (build)
2Fix the reviewer role: either add it to user_role via migration (and fold into baseline) or remove it from the type, validators and UI. Add a test that compares TypeScript unions with database enums.🟠 HighS
3Delete the unreachable classic pages and specs already marked for deletion; document the orphaned tables as a schema-cleanup migration.🟑 MediumS–M
4If non-English markets are in scope, introduce an i18n framework and a single date/number formatting helper; start with formatting (already partly centralised in src/lib/utils.ts).🟑 MediumL
5Obtain and bring into the data room the externally held product direction, open-decision ledger and compliance posture.🟑 MediumS
6Validate market positioning and customer evidence through commercial due diligence (customer calls, contract review).🟑 MediumM