Mock testing — backend and frontend
Everything that leaves the process is mocked, except the database. The database stays real because tenant isolation is our hardest correctness property, and a mocked Prisma proves…
We’re documenting the system behind OmniChan — what we’ve shipped, what’s in build, and how each capability is verified.
Back to OmniChan
23recorded
decisions
49dated
milestones ahead
86/170
features shipped
3860/3863
tests passing
170 capabilities tracked
86 Shipped44 In Build40 Planned
The gate is the product’s spine, so here it is working. Choose who proposes, send it to the queue, and decide. The audit log fills in as you go — and the one control that would skip it does nothing, because in the API that transition does not exist.
Cut bids on 6 losing keywords (−£310/mo)
Run run_8f2a · 4 steps · £0.004 · full diff attached
Append-only. Every row carries the actor that caused it.
Marketing runs on pictures, so two of these features are about handling them properly: one library every channel draws from, and a pipeline that turns a single upload into every size an ad platform demands.
upload once, tag, reuse — thumbnails and transcodes on the way in
one upload, the five sizes Google Ads asks for

ss26-af1-pastel.jpg
1800×1800 · 261 KB · uploaded once
Search · Display · PMax

Display · Discover

PMax · Demand Gen

Responsive Search

Demand Gen vertical

Generated with sharp on upload, not on request — an ad platform rejecting a campaign at 4pm because a derivative was still rendering is not a trade worth making.
Every platform implements the same interface, so adding one is a single file and a registry row — the publisher, the inbox and the analytics never change. Each row names the feature that builds it, and shows that feature’s real status.
| channel | track | notes | feature | status |
|---|---|---|---|---|
| organic | Pages + personal posting | F-041 | done | |
| organic | One Graph surface, two adapters | F-042 | done | |
| organic | Shares the Graph adapter | F-042 | done | |
| YouTube | organic | Uploads + PubSubHubbub | F-043 | done |
| X | organic | After the first four are proven | F-044 | in progress |
| Threads | organic | After the first four are proven | F-044 | in progress |
| TikTok | organic | After the first four are proven | F-044 | in progress |
| organic | After the first four are proven | F-044 | in progress | |
| Google Business | organic | Local profile posting | F-044 | in progress |
| Google Ads | paid | OAuth, discovery, campaign read/write | F-050 | in progress |
| Meta Ads | paid | Parity with Google Ads | F-051 | in progress |
| GA4 | analytics | Site-side truth to reconcile against | F-064 | done |
| OpenSEO | analytics | Keywords, ranks, backlinks, AI visibility | F-091 | in progress |
| Apollo | analytics | Company + contact enrichment | F-092 | planned |
| messaging | Opt-in only; Baileys bridge | F-048 | in progress |
The interesting parts of this build are the ones nobody has an answer to. SEO has rank; AEO has no equivalent. Metered APIs bill per call, so how little data can we get away with? Does a human actually read a 40-line agent diff? Each question is recorded before its answer, with the hypothesis it will be judged against.
brightbean rendered the composer preview server-side via a debounced HTMX POST, which forced the preview endpoint to stay stateless and added a round-trip per keystroke pause. Rendering per-channel previews client-side should remove both constraints.
Settled at design time, not by measurement: with React the preview is a pure function of the draft, so there is nothing to fetch. Recorded here because it is a deliberate divergence from the source app, not an oversight.
Groq returns a usable multi-channel draft fast enough that the composer can regenerate on keystroke-idle rather than behind an explicit 'Generate' button. If p95 is under ~800ms the interaction can feel live instead of batch.
Not run yet — this one is still an open question.
A 15-second polling worker against a Postgres jobs table holds up fine at our volume (tens of posts a day, not thousands), so Redis never needs to enter the stack. Measure lock contention and publish-time drift under a synthetic 10× load before committing.
Not run yet — this one is still an open question.
Neither source app had benchmarks and there is no obvious free authoritative feed. Options to evaluate: published vertical reports (stale but free), the platforms' own auction-insights and competitive-metric endpoints (live but narrow), or our own historical baseline (honest but only after months of data). Likely a blend, clearly labelled by source.
Not run yet — this one is still an open question.
An approval queue that shows a raw diff of 40 bid changes will be rubber-stamped, which defeats the gate. Test whether grouping by rationale ('3 changes to cut spend on losing keywords, projected saving X') produces measurably more rejections than a flat diff — i.e. whether people are actually reading it.
Not run yet — this one is still an open question.
Platform-reported and GA4 conversion counts will disagree, sometimes by a lot. Rather than pick a winner, show both with the delta and a plain-language reason (attribution window, view-through, consent-mode modelling). Establish the typical delta range before the KPI surface ships, so an anomaly is recognisable.
Not run yet — this one is still an open question.
SEMrush bills per API unit, so a naive 'refresh on page load' would be expensive fast. Test whether a weekly pull per project domain, cached, is fresh enough for the decisions people actually make — keyword sets and competitor positions move slowly.
Not run yet — this one is still an open question.
SEO has rank. AEO has no equivalent: being cited by an assistant is not a position on a page. Candidate measure is citation rate across a fixed panel of prompts per assistant, sampled weekly — establish whether that is stable enough to steer on, or too noisy to be a metric.
Not run yet — this one is still an open question.
SEO tools produce lists nobody actions. Opening the fix as a pull request against the project's repo puts it in the workflow that already has a review gate. Test whether PR-shaped fixes get merged at a materially higher rate than the same fixes listed in the UI.
Not run yet — this one is still an open question.
Hootsuite prices listening on breadth — 30+ networks, 300+ review sites, 150M websites — which is a selling point for a product with thousands of customers and mostly waste for one organization. Scoped to Verjson's brand terms and a named competitor set, the query volume may be small enough that a metered API beats a subscription by an order of magnitude. Price both before committing, the same way OpenSEO turned out cheaper than SEMrush.
Not run yet — this one is still an open question.
Marketing tools are dull for a reason worth naming, not just a lack of colour. The working guess: they present every number at equal weight, so nothing on the screen is ever surprising or actionable - the product asks the user to do the noticing. If that is the cause, the fix is editorial rather than visual: say what changed and what it means, and let the rest recede.
Not run yet — this one is still an open question.
Computed from the board, not written here. Nothing without a date appears — an undated intention is not a next step.
Everything that leaves the process is mocked, except the database. The database stays real because tenant isolation is our hardest correctness property, and a mocked Prisma proves…
A workspace is a brand or a client — the scope unit below an organization. Social accounts, projects and media all belong to one.
Store UTC, render in the project's zone, schedule DST-correctly. Built before the calendar, not retrofitted after.
One GitHub consent covers both sign-in and the repo integration.
Month, week and list views. Drag to reschedule, per-channel queues, recurring slots, timezone-correct across DST.
Upload, tag and reuse. Thumbnails via sharp, video transcodes via FFmpeg, processed before publish rather than on upload.
49 dated milestones across 6 months · undated work is deliberately absent
All 170 features, including the 40 still planned. An internal front door that showed only the finished ones would be a brochure.
npm workspaces: @verjson/api (Hono, owns Prisma and every DB credential), @verjson/web (Next.js, no DB driver), @verjson/contracts (zod schemas both import). The seam is enforced by no-rest…
bcrypt password hashing, jose-signed HS256 access and refresh tokens, rate-limited signup and login. Login returns the same 401 for an unknown email and a wrong password, so the endpoint ca…
Signup creates an organization and its owner user in one transaction. Every subsequent query is org-scoped; a cross-tenant read must 404 rather than leak.
A workspace is a brand or a client — the scope unit below an organization. Social accounts, projects and media all belong to one.
owner · admin · member · agency · client. Write access is limited to internal roles; only an owner or admin can manage who is on a project.
Outside agency staff get their own logins, scoped to the projects they are assigned — never the whole organization. Their work flows through the same approval gate as everyone else's.
Stakeholders review and approve content without an account, via a signed magic link. 32-byte token stored as a SHA-256 hash.
An external contributor composes a post from the calendar and suggests a date. It lands draft with the time on proposed_publish_at, never scheduled_at — the publisher selects on a variant w…
Teams (Design, Content, Video), individual roles (Video Editor, Carousel Designer) and post assignment with status and due date. A team is a grouping, never an access grant — project_member…
The aggregate of every journey, built from the tracker's own pageviews — no third-party analytics. Entry pages ranked by volume, next steps per depth, and leaving drawn as an explicit edge…
AES-256-GCM encryption at rest with per-purpose keys derived by HKDF, plus one-way hashing for tokens. Live and in use by OAuth; the credential-vault UI is still to come.
Job intervals, rate limits, approval modes and per-project defaults, editable without a deploy.
First-run wizard: connect channels, set the timezone, invite the agency, create the first project.
Append-only trail of security-relevant actions: actor, action, entity, metadata, IP. Every agent-initiated action lands here too, with the key that made it.
In-app, email and Slack. Triggers: approval requested, publish failed, tracking broke, spend anomaly, low budget.
Postgres-backed fixed window, shared across instances, degrading to an in-memory bucket if the DB write fails — so a transient database hiccup never locks users out.
S3-compatible put · get · delete · presign, against MinIO locally and managed object storage in production. Keys are sanitised so a filename cannot inject path separators.
Google and GitHub sign-in alongside email/password. State is a signed, short-lived JWT so the cross-site callback cannot be forged; accounts link only on a provider-verified email; provider…
Four plans, Stripe Checkout, the hosted portal, and signature-verified idempotent webhooks. Entitlement is derived from the subscription, so a cancellation degrades to free limits rather th…
The top-level unit of work. Create a project, pick its marketing mix, and put a budget behind each channel. Everything else — campaigns, calendar entries, KPIs, agency assignments — hangs o…
Month, week and list views. Drag to reschedule, per-channel queues, recurring slots, timezone-correct across DST.
A brief goes in; a channel mix, budget split, posting cadence and messaging direction come out — as a draft plan a human edits and approves.
Structured intent: audience, offer, constraints, budget, success metric. The input to plan generation and the record of what we meant to do.
One calendar, three tracks. Each has its own approval rules — an influencer brief and a paid campaign do not need the same sign-off.
Twelve channels — SEO, AEO, paid search, paid social, organic social, email, WhatsApp, content, influencer, affiliate, PR, events. A project declares which it invests in, and a budget may o…
One amount per channel per period, as integer minor units. Re-setting a channel and period replaces the amount rather than stacking rows, so the total is never a sum nobody can reason about.
spent_minor filled from real platform spend by the ingestion job, never typed in. Drives the over-budget state and the hard cap.
Write once, override per channel. Live preview, per-platform character limits, media attachment, first-comment support.
Groq-backed drafting from a brief and a channel. Output is always a draft — the composer never publishes.
Make a video from a script - ElevenLabs voiceover, rendered and sized per channel - and edit an existing one: trim, caption, re-crop for another aspect ratio, swap the audio.
Upload, tag and reuse. Thumbnails via sharp, video transcodes via FFmpeg, processed before publish rather than on upload.
A taxonomy that planning and reporting both use, so 'how did thought-leadership perform?' is answerable.
Multi-step, per-track approval. Nothing reaches a third-party platform without a recorded approval — including anything an agent proposes.
Named time slots per channel, recurring posts, and a queue that fills the next open slot.
The outbound worker. Retries with backoff, partial-failure handling when one channel of a multi-channel post fails, idempotency so a retry cannot double-post.
Attribution that survives the click, applied automatically from the project's convention.
Recoverable for 30 days, then purged.
One interface every platform implements. Adding a channel is one file plus a registry row — no change to the publisher, inbox or analytics.
Publish, metrics, comments. No inbound webhook support — polling only.
One Graph API surface, two adapters. The only two channels where inbound webhooks meaningfully replace polling.
OAuth, resumable video upload (session init, byte upload and cursor probing so a dropped connection resumes rather than restarts), per-video metrics, and comments pulled in by polling.
Added once the first four are proven. Each is one adapter file.
Per-platform OAuth flows, encrypted token storage, hourly refresh for tokens expiring within 24h, and a health check that flags a disconnected account before a publish fails.
Comments, mentions and DMs across every connected channel, in one queue, with sentiment tagging and reply-in-place.
HMAC-SHA256 verified receivers for Meta, PubSubHubbub for YouTube. Signature is checked before anything is parsed; unsigned requests get a 403.
Templates, opt-in management, broadcasts and two-way conversations through a Baileys bridge running as its own container, behind the same ChannelProvider interface as every other channel.
OAuth, account discovery, campaign read/write via the Google Ads API.
Parity with Google Ads, built to the same AdsProvider interface.
Our CPC, CTR, CPA and ROAS against vertical benchmarks — the 'are these numbers actually good?' answer that a raw dashboard never gives.
Multi-step campaign build for SEARCH and PMAX, with the asset slots pre-filled by the AI composer.
Detect broken or absent conversion tracking and generate the fix. Silent tracking failure is the most expensive bug in paid media — it wastes spend without producing an error anywhere.
One upload becomes the five image sizes Google Ads requires, via sharp.
Daily metric pull → analysis → proposed changes (bids, budgets, pausing losers) → approval gate → apply.
Per-project budget, spend-to-date across platforms, and a hard cap that pauses rather than warns.
Feed qualified leads back to the ad platforms as a bid signal.
Launch, pause, resume and adjust budgets on live campaigns from inside the studio, with the platform's real state polled back so the UI never shows a stale 'running'.
Reach, engagement, follower growth and post-level performance per connected account.
Roll-ups and comparisons across channels and projects.
Every channel, organic and paid, on one screen. The headline ask: answer 'what is running and what is it returning' in under 60 seconds.
Composable sections, scheduled delivery as PDF and email.
Site-side truth to reconcile against platform-reported numbers, which routinely disagree.
Every number carries its as-of time. A stale number that looks live is worse than no number.
The same /api/v1, documented for machine consumption. Agents get no privileged side door.
Streamable-HTTP transport so Claude Code, Codex and other MCP clients connect directly.
Hashed at rest, scoped to a project and a permission set, revocable, with last-used tracking.
Multi-step runs — plan → draft → schedule → measure — as a resumable state machine, so a failure mid-run does not lose the work before it.
Agents propose; a human approves. Enforced by the state machine rather than by convention — there is no transition from `running` to `applied`, and an agency contributor can see a run on th…
What ran, what it changed, what it cost, and which key authorised it.
postgres + minio for day-to-day; a --profile apps mode that runs the full four-container production topology locally.
web · api · worker Deployments, managed Postgres, S3-compatible storage, Ingress with TLS. Migrations run as a pre-upgrade Job, not on container start.
typecheck → lint → test → build, on every push.
Everything that leaves the process is mocked, except the database. The database stays real because tenant isolation is our hardest correctness property, and a mocked Prisma proves nothing a…
Daily pg_dump to object storage with a manifest, 30-day retention, and a restore command that has actually been run.
Per-workspace data export and deletion.
Across posts, campaigns, media and projects.
Store UTC, render in the project's zone, schedule DST-correctly. Built before the calendar, not retrofitted after.
This board. Seven tabs — Overview, Features, Flows, Architecture, Tests, Labs, Docs — all reading from data/dashboard/*.json and docs/*.md. No database, so it works from day one and is diff…
The internal front door. Seven interactive sections, every number and status read from this dashboard data: an approval-gate simulator, a ⌘K palette over every feature/experiment/doc, a tab…
Tokens in globals.css, primitives in components/ui, an animation library, and a dev-only Style Lab at /style-guide that writes a chosen theme back into the stylesheet.
Keyword rank tracking and backlink monitoring against live data, via DataForSEO's organic SERP and Backlinks endpoints behind modules/growth/. Both fall back to deterministic mocks when no…
Company and contact enrichment for outbound: build a target list from a project's ICP, enrich it, and push it into email or WhatsApp sequences.
Crawl a project's pages and report the fixable things: titles, meta, headings, canonicals, structured data, internal links, Core Web Vitals, broken links.
Seven AEO rules over the same crawled pages, kept separate from SEO because the target differs: ranking asks 'is this a good result?', citation asks 'can a model find a self-contained claim…
Connect a project's repo: see which pages exist in the codebase, open content and SEO/AEO fixes as pull requests, and read deploy status so a recommendation lands as a reviewable diff rathe…
Direct exchange, one durable queue per job kind, a broker-side delayed retry queue and a dead-letter queue. Worker is the same image as the API with a different command; WORKER_KINDS lets a…
Durable multi-step runs — research, draft, wait for approval, schedule — that survive a deploy and can retry from a step rather than the beginning.
Unified read/search across ~14 platforms (X, Reddit, LinkedIn, YouTube, GitHub, RSS, podcasts, web search) via its MCP server, for competitor and audience research.
Fetch-and-parse as a queue consumer, so it inherits retries, dead-lettering and rate limiting rather than growing its own scheduler. robots.txt respected, per-host politeness delay, honest…
/docs — a manifest-driven documentation site. data/docs/manifest.json holds the navigation and points each page at a markdown file, a generated view, or nothing yet.
Recommended send times per channel, derived from this project's own engagement history rather than a generic industry table.
Import a quarter of planned posts from CSV, preview the parsed rows, and queue them against calendar slots.
A mailing list the project owns, plus the mail sent to it. Contacts arrive by drag-and-drop CSV import and are filed into audience segments — the three built-in ones (Active Clients & Buyer…
Per-project audience categories with a name, description and colour, created from the Audience header and sitting beside the three built-in segments as filter cards with live counts. Contac…
A second source tab in the import modal: paste a Google Sheets link instead of uploading a file. The fallback-segment picker, the auto-create-categories switch and the result summary are sh…
Replaces the four-field draft modal with a Gmail-shaped composer: a formatting toolbar, hyperlinks with their own display text, inline images, file attachments, personalisation tags that wo…
Font, size, bold, italic, underline, strikethrough, text colour, highlight colour, alignment, bulleted and numbered lists, indent/outdent, blockquote, undo, redo and clear formatting. Built…
Images go into the body by toolbar upload or by drag-and-drop, through the shared `POST /uploads` endpoint that already enforces the accepted MIME types and the per-kind size caps; they lan…
`{{first_name}}`, `{{last_name}}`, `{{full_name}}`, `{{email}}` and `{{company}}`, insertable at the caret in the subject line or the body from a picker in the toolbar. Every tag carries a…
A Desktop (640px) and Mobile (375px) preview, rendered in an iframe with `sandbox=""` — a foreign origin with no scripts, showing sanitised HTML at the same measure the dispatcher wraps the…
Drafts autosave into a real broadcast row from the first save — created, then PATCHed — rather than living in component state or localStorage, so a closed tab, a dead battery and a differen…
The ad-set builder's pixel field is a dropdown of the ad account's own pixels, fetched from Meta's `adspixels` edge through `GET /ad-accounts/:id/pixels`, each shown with the date it last f…
`POST /track/conversion` also sends the conversion server-to-server to Meta's Conversions API. The browser pixel is the least reliable part of the measurement stack — Safari's ITP caps its…
Reusable responses with variable substitution, so common inbox replies are one click rather than retyped.
Brand mentions, sentiment and emerging trends across the channels we care about — scoped to our own brand and a named competitor set, not to the whole internet.
A named competitor set's posting frequency, engagement rate and follower growth, next to ours.
A feed of approved posts that staff and agency contractors can reshare to their own networks, with attribution back to the source post.
Sentiment aggregated over time and by channel, turning per-message tags into a trend.
Email a report on a schedule, as PDF and inline summary.
Google Business, Trustpilot and app-store reviews alongside social mentions, with replies from the same inbox.
Who follows an account, per platform: age, gender, country and city, fetched from the platform rather than illustrated. An OPTIONAL getAudienceDemographics on ChannelAdapter feeds AccountAu…
Invite someone straight onto one project: Team -> Invitations asks whole-workspace or one-project, then a project and a project role. The accepted user lands on that project and sees no oth…
One ORG_WIDE_ROLES in core/org-scope.ts instead of eleven copies, and `member` narrowed to the projects they are a member of. Owner and admin stay org-wide; everyone else sees only what the…
Lifetime value per customer, surfaced for quick access.
Publish and measure Stories, not just feed posts.
See which posts each team member created, so contribution is visible rather than assumed.
Let an approver fix a post themselves instead of bouncing it back, with every edit recorded.
Click a point on a chart to filter the metrics and reach the posts behind it, instead of reading a number and going hunting.
Watch what is being said about the brand across platforms, not only what arrives in our own inbox.
Track competitors' posting, engagement and positioning alongside our own.
Classify posts into buckets - educational, engagement, self-care and so on - show the current percentage split, and recommend an allocation.
Re-derive the bucket split on a cycle from what actually performed, so the mix follows evidence rather than the first guess.
Every customer complaint - whichever platform it arrived on - collected into one view with its own section.
Score incoming messages, comments and reviews by sentiment so the angry ones surface first.
Map the real path into the brand - Instagram, website, Google reviews, TripAdvisor - so messaging can vary by stage rather than being uniform, and targeting and retargeting can follow it.
Answer which brand suits a given customer and which platforms they prefer to buy on, and why.
A small breakdown of who is buying - age and region - derived from phone numbers and emails.
A place where product updates are announced to the people using the studio.
Pull reviews from Google, Amazon and Blinkit, score them, and reply without leaving the studio.
Bring email, Instagram DMs and WhatsApp into the same inbox as everything else.
One page saying what the product can actually do, kept honest.
Match setup to the customer's industry and use case rather than presenting every feature to everyone.
Work out how budget should be split across platforms, from measured return rather than habit.
Multi-step email sequences - triggered, scheduled and measured - rather than single broadcasts.
Connect Google Business Profile for posts, reviews and the local presence.
Turn one piece of content into the formats other channels want, instead of authoring each from scratch.
Take one image and produce the sizes each channel wants - square, portrait, story, landscape - with a crop the user controls rather than a centre crop that beheads people.
Tags become rows rather than a column, so renaming one cannot orphan the contacts carrying it and a segment can join on it. Per-project custom fields with sparse values, richer contact fiel…
Sending moves to the Resend API for its event stream: delivery, bounce and complaint webhooks plus signed-token open and click tracking, an append-only event log, per-project suppression co…
Three screens over records the API already kept and the UI never showed: an Overview that opens the section with a worst-first list of what needs attention, a Templates screen for EmailTemp…
New automation opens a gallery — five complete journeys, the blank builder, or the assistant — instead of a form and an empty canvas. A recipe is a declarative tree flattened into the same…
Under every trigger picker, a sentence saying who is enrolled and — separately labelled — the audience that exists today, because in this engine nobody standing enters: triggers are raised…
Nothing raises the segment_entered trigger, so a journey on it validates, activates and never starts. AUTOMATION_TRIGGERS_NOT_YET_FIRING now carries the explanation, which the builder and c…
Pick a contact and see the exact path they would take through a draft journey — which branch each condition sends them down, and why they would not enter at all. Needs a server endpoint tha…
Two engine features, not screens. Exit conditions leave a journey as soon as something becomes true wherever the contact is in it (the classic 'they ordered, stop the cart series') — today…
The graph and the worker that walks it. A contact sits in an automation rather than running it: an enrollment row holds the current node and a next-run time, and a sweep claims due rows eve…
A React Flow canvas over the engine's graph - drag, connect, duplicate, delete, per-node editors, save as draft, activate - plus the two screens that make a running automation debuggable: a…
A form builder, a public submit endpoint, and the form-submitted trigger: check the address, create or update the contact, apply tags, record the source, enrol. The drag-and-drop landing pa…
One Meta login connects every Facebook Page and linked Instagram business account the user granted, each as its own account with its own Page token; analytics dashboards can switch between…
A project can register several apps for one platform (e.g. one Meta app per client brand), choose which to connect through, and every account keeps using the app that connected it.
Drag-and-drop landing pages: a block palette (hero, text, image, button, sign-up form, features, spacer, divider), drop-into-gap insertion and handle reordering, desktop/mobile preview, the…
Prepares everything the email and automation screens can do — contacts (one or up to 50), tags, segments, drafts, test sends, sends, branching journeys and step edits — plus social posts an…
The assistant says how an email, an automation, a social channel or the website did — against the project's own usual — attaches images and video sent in the chat to posts, and writes in a…
Change or reschedule an existing post under the project's approval rule, best posting and sending times from the project's own data, images drawn on Confirm, and a private memory carried ac…
Measure before tuning: one Lighthouse run on a logged-in page, @next/bundle-analyzer on a production build, a Server-Timing header from the Hono handler wrapper, and Prisma/Postgres query-d…
meterApiCall awaits a usageMeter.upsert after next(), so every authenticated response waits on a write, and the upsert on one row per org serialises concurrent requests. The session middlew…
main.ts starts ~20 interval workers (publisher, metrics, crawl, keywords, backlinks, AI citations, ad metrics, automations, reconciliation, usage reconcile, inbox polling, KPI rollups, repo…
Prisma defaults to 5 connections shared between requests and workers; Postgres runs on container defaults (128 MB shared_buffers). Set connection_limit explicitly, raise shared_buffers/effe…
mermaid, @xyflow/react, recharts, react-markdown and motion are all statically imported and the app has no next/dynamic at all. Load the dashboard markdown renderer, the automation canvas a…
235 of 270 findMany calls carry no take. Most are lookup tables; posts, notifications, audit log and metrics are not and will slow linearly. Add cursor pagination to those endpoints and the…
The bearer lives in localStorage, so 89 of 105 pages are client components and every hard load is shell → JS → hydrate → get-session (blocks the app shell) → page queries. Move the session…
115 enabled: chains in apps/web/src/lib serialise page data. Prefetch the shared pieces (project, campaigns, accounts) in the project layout and add a per-page bootstrap endpoint where a vi…
37 raw <img> tags against 5 next/image usages and no images config: no resizing, no lazy loading, layout shift on MinIO uploads. Add remotePatterns for the storage origin and convert the me…
Four places in apps/api/src/modules run a Prisma query inside a loop or an async map. Replace each with one findMany over the collected ids.
The root layout loads four families and thirteen weight files. Geist is only used under .app-theme and Inter only on the public side; load each in its route group's layout and drop the weig…
The web Dockerfile copies the full node_modules tree and runs npx next start; output: 'standalone' cuts memory and cold start on the 4 GB droplet. Confirm cf-cache-status is HIT on /_next/s…
reactCompiler is off and the app has a single memo() call; enable it behind the baseline numbers to see whether re-renders matter at all. Run depcheck once and drop unused packages. Five in…
The product is Clearhouse. Ported palette (an action blue and an identity blue, a brand neutral ramp, magenta/vermilion/sky accents), four type families, a fixed page gradient and the glass…
A shared thread per project (ProjectMessage), distinct from the assistant's per-user conversation. People post; the product posts too — an approval, a rejection, a changes-requested, a publ…
Two tours: eight stops around a project and four around the workspace, anchored to real nav rows by data-tour attributes and restartable from the user menu. It never clicks or navigates for…
The Team screen answers "who is on this project"; this answers the opposite question — "what can this person reach" — which is the one asked when somebody joins, changes role or leaves. A p…