2026-10-10¶
| Who | Stuart Meeks; Claude (Cowork, then Claude Code) |
| Pull requests | #1, #2, and the pull request that adds this entry |
What we did¶
- In a Cowork session, chose the name (Signboard, from kanban, "signboard" in Japanese), set up the GitHub organisation and scaffolded the documentation site: vision, PRD and design doc skeletons, and ADRs 0001–0006. Merged as #1.
- Cowork had no GitHub credentials, so it wrote a private handoff note and work moved to Claude Code in Stuart's terminal.
- Added the Docs workflow, which runs
mkdocs build --strictwhen documentation changes (#2). - Added a Docs / journal check that fails any pull request changing the repository without updating the journal, unless it carries the
no-journallabel. It went into this pull request rather than #2, because #2 could not pass it without the journal that this pull request introduces. - Decided the licence, chose multi-tenant SaaS, chose the stack and wrote down the project's core principles as ADRs 0007–0014.
Decisions¶
- Licence: Apache 2.0, by Stuart (ADR 0003). "This is not a money-making exercise."
- Accounting behind a plugin interface and configuration over customisation accepted, by Stuart (ADR 0005, ADR 0006).
- Tenancy: Claude first reframed ADR 0004 as one instance per shop, run by the shop or a hosting operator. Shown the full text, Stuart chose multi-tenant SaaS instead (ADR 0004). Isolation model (shared schema, PostgreSQL row-level security plus EF Core query filters) proposed by Claude and accepted by Stuart.
- Back end .NET, database PostgreSQL, front end React and TypeScript, by Stuart (ADR 0007, ADR 0008, ADR 0009). Mantine as the component library proposed by Claude.
- API first, documentation first, testing with a 95% coverage gate, work in the open, and this journal, by Stuart (ADR 0010–ADR 0014).
- Customer portal for quote and proof approval moved into phase 2, by Stuart.
- The workflow is named "Docs", not "CI", which is reserved for code builds, by Stuart.
What the AI got wrong or was overruled on¶
- Claude recommended AGPL-3.0 to protect against closed hosted forks. Overruled: protection against commercial use was never a goal, and AGPL would have bound a hosted offering by the maintainer once outside contributions were merged.
- Claude first suggested a server-rendered stack (Django, Rails or Laravel) and then Blazor, to keep a non-front-end developer in one language. Overruled in favour of React for its ecosystem.
- The original ADR 0004, written in the Cowork session, was titled "Self-hosted, single tenant". That conflated who runs an instance with how many shops share one, and would have quietly ruled out a hosted offering. Stuart's doubt surfaced it.
- Claude then recommended one instance per shop over multi-tenancy, and listed multi-tenant SaaS as too costly for a part-time project. Overruled: Stuart wants a hosted service, and tenancy is a foundation that is far cheaper to design in than to retrofit. Claude's own "plan the foundations" principle argued for Stuart's choice.
- Claude proposed no coverage percentage, only acceptance-criterion traceability. Overruled: a 95% gate on every code pull request, from the start.
-
The handoff note said the scaffold branch was unpushed. By the time Claude Code started, #1 had already been merged. Claude checked the repository rather than trusting the note.
-
Claude wrote ADR 0010 assuming each user belonged to one shop, and that a customer of two shops had two identities. Stuart corrected it: users are global and keyed by email, and access comes from memberships of one or more shops and one or more customers. ADR 0004 had the same flaw and was corrected with it.
- Stuart then settled the account model: three kinds of account (shop, customer, operations); a customer belongs to one shop; one customer portal per shop. Operations users get no implicit access but can add themselves to an account. Claude had written support access as granted by the shop; Stuart's rule replaced that, with visibility and audit kept as safeguards. Claude suggested notifying the shop and expiring self-added memberships; Stuart accepted the notification and rejected expiry.
- Renamed the tenant from "shop" (sounds like an online store) to business, by Stuart. Claude argued against "Vendor" (a near-synonym of a possible future supplier account, and accounting software such as QuickBooks uses it for suppliers) and "Operator" (machine operators are everyday staff in a sign shop).
- Walking through ADR 0009, Stuart challenged serving the UI from the API's host. Claude's single-container reasoning dated from the single-tenant design and had not been revisited after the move to multi-tenancy. Agreed instead: separate deployments served from one origin through an edge router, which keeps cookie sign-in and future custom domains working (ADR 0015).
- Also agreed: the browser signs in with a cookie and integrations with tokens, on the same API; the customer portal is the same app as the staff UI.
- Stuart required full RQL support on the API, in the Marketplace dialect, with grids built around it. Claude drafted ADR 0016 using Mpt.Rql, after checking its licence (Apache 2.0), recent releases and source. Claude raised the risk of inferring hidden values by filtering on them; Stuart agreed that field visibility must cover filtering, sorting and selection.
- Walking through the rest of ADR 0009: Claude had recommended Mantine React Table for grids. Checking before the decision showed it had stalled (version 2 in beta since February 2025, built for Mantine 7 while Mantine is at 9). Agreed instead: TanStack Table inside Signboard's own
DataGrid, a wrapper rule so screens never import Mantine directly, touch support as a hard requirement for the kanban board, and persisted table views (Stuart's addition). Claude proposed storing them through the API so they follow the user; Stuart chose browser storage, scoped to the user, with business default views set by an administrator and stored through the API. ADR 0009 accepted. Business default views may include filters, shown as removable chips; named views deferred. -
Object IDs (ADR 0017): Claude proposed two identifiers, a UUIDv7 primary key behind a public prefixed ID, plus separate document numbers. Stuart wasn't convinced and asked Claude to make the case. Claude dropped the hidden UUID, which was insurance against a change with no reason to expect it, but argued for document numbers. Stuart chose one object ID only (
JOB-4821-0937-1156, random digits, collision-checked), with additional identifiers scoped to actors left for a later decision. Claude pointed out that a lookup before inserting would miss collisions hidden by row-level security, so the primary key constraint is the check. Stuart then set the digit length per object type, in multiples of four, decided in each type's requirements; made soft deletion the default for every row, which Claude recorded as ADR 0018; and decided a quote keeps one ID with numbered versions. -
Audit trail (ADR 0019): Stuart asked for a global audit trail and added one requirement, a full JSON snapshot of the object after each change instead of before-and-after values, with diffs done in the UI. Short of time, he asked Claude to draft the rest from its recommendations for later review. It stays Proposed, with the open decisions listed in the ADR.
-
URL structure (ADR 0020): Stuart had little to add and asked to see a draft. Claude drafted hosts, API paths and app routes from the earlier decisions. Stuart changed flat paths to paths grouped by module, accepted
PATCHoverPUT, and was unsure whether the business and customer portals would differ enough to need separate URLs. Claude suggested one URL per object for every actor, with the screen chosen by account kind, which removes the need to decide before mock-ups. Stuart accepted the starting modules and app URLs that mirror them; ADR 0020 accepted. -
Stuart added that every request on an object returns the full object, with hidden fields omitted. Claude first recorded it as a separate ADR 0021 rather than editing the accepted ADR 0020. Stuart then relaxed the rule: during initial design, accepted ADRs may be edited directly, and superseding applies once code implements them (ADR 0001). Claude had been applying the strict rule since the first ADRs were accepted, adding records where an edit would have done. ADR 0021 was then folded into ADR 0020 as its Responses section. Claude proposed leaving 0021 unused; Stuart saw no point, so the next ADR took the number.
-
Authentication (ADR 0021): Stuart accepted a central OpenID Connect identity service with single sign-on across business hosts, built on OpenIddict ("we'll move later if we need to"). He allowed passwords as an option, which Claude had proposed leaving out, on condition of mandatory MFA. Email links to customers carry no credentials; one sign-in per shared tablet for now; Google and Microsoft sign-in later.
-
Authorisation and roles (ADR 0022): Claude proposed code-defined permissions, roles as bundles, several roles per membership, and field groups rather than per-field permissions. Stuart chose built-in roles only for now, management of customer users by both the business and the customer, a read-only support role for operations users who add themselves, a simple operations role split, and deferred limits and approvals.
-
Email sending (ADR 0023): built on Stuart's earlier SES and SNS preference. Claude added separate platform and business streams, because sign-in now depends on email, and recommended against open tracking. Stuart chose a fallback sending domain until a business verifies its own, PDF plus link for documents, SES in Sydney, and SMTP as a supported option for self-hosters.
-
Product analytics (ADR 0024): Stuart asked for something like Heap, free if possible. Claude recommended PostHog Cloud with named events only, no business data and replay off, and flagged that its free-tier limits were quoted from memory and need checking. Stuart chose to track everyone, operations users included, with self-hosted installations sending nothing by default. Claude added a per-user opt-out and the EU region.
-
Observability (ADR 0025): Stuart asked for logging, and chose all four signals (logs, traces, metrics, alerts) when Claude offered the wider scope. The backend waits for the hosting decision; front-end errors go to PostHog; retention 90 days rather than the 30 Claude suggested; alerts by email with nothing out of hours.
-
Modular monolith (ADR 0026): Stuart asked for Claude's lean before giving his own. Both independently favoured a modular monolith with enforced module boundaries and a few deployables from one codebase.
-
File storage (ADR 0027): one files component for every module, as Stuart proposed. Stuart set a 50 MB limit; Claude flagged that some print-ready files will exceed it, so the limit is configuration and is to be reviewed with the design partner shop. Customer uploads from launch (so scanning is required), previews later, sent documents kept exactly, cheaper storage tiers for old files.
-
Published documentation: Stuart found the docs readable for the team but not for anyone else, and asked about GitBook. Claude pointed out that the MkDocs Material site was already being built by CI on every change and then thrown away, so the gap was publishing, not tooling. GitBook would have added a second editing path outside pull requests, a different Markdown flavour and a dependency on its free open-source plan. Stuart agreed. The Docs workflow now deploys the site to GitHub Pages at
docs.signboard.workson every push tomain, andsite_urlwas corrected (it pointed atsignboard.works).
Lessons¶
- Handoff notes go stale within hours. Check the repository state before acting on one.
- An AI assistant tends to optimise for the maintainer's existing skills. The maintainer's priorities (ecosystem, future hosting) can outweigh that, and need saying out loud.
- Check a library's current state (releases, compatibility, maintainers) before recommending it, not after. Training-time knowledge of the JavaScript ecosystem goes stale fast.
- When a foundational decision changes, re-check every decision that was reasoned from it. The single-host choice survived the move to multi-tenancy only because nobody looked again.
- Identity and tenancy are separate questions. Users are global; data is tenant-scoped. Don't let one decision quietly decide the other.
- Path filters on GitHub Actions triggers break required status checks. Filter inside the job instead.
Next¶
- Decide sign-up, plans and billing in their own ADR and PRD.
- After merge, make Docs / build and Docs / journal required status checks on
main. - Questions for the design partner shop are still open: accounting system, volumes, data export from the old system.
- Start the pricing engine design doc.