YSBDesigns

Engineering

Pipelines, data and the failures that taught me.

Every claim on this page points at a file in a public repository \u2014 a workflow, a migration, a config. Where something is built but not switched on, it says so.

Delivery

Eight workflows across one app repository, plus a deploy pipeline per site. The interesting ones are not the builds \u2014 they are the gates.

  1. 01 \u00b7 native-release.yml

    Release gated on a real device

    The Play bundle workflow refuses to run unless that exact commit already has a successful smoke run against it. It asks the GitHub API for runs of native-smoke.yml at head_sha and exits non-zero if the count is zero. The failure message says why the gate exists: a bundle that packages correctly and crashes on launch passes every other check in the workflow.

    It exists because that is precisely what happened — five green builds that were a red error screen, which is the entry below.

  2. 02 \u00b7 android-build.yml

    Verifying against the source of truth, not the artefact

    Before building, the job asks Google’s Digital Asset Links API whether the live site actually verifies the app, rather than trusting the assetlinks.json sitting in the repository. The two can disagree, and only one of them decides whether a store install opens without a browser bar.

    A file in git is a statement of intent. The API is the fact.

  3. 03 \u00b7 native-build.yml, native-smoke.yml

    Contract tests across two codebases

    The native app and the website are separate repositories that must not drift. CI asserts that the app says what the website says — in English too — that every link points where the website points, and that every colour it draws matches. A screen-reader naming check runs in the same job.

    Two surfaces describing the same masjid differently is a content bug nobody would catch by hand.

  4. 04 \u00b7 deploy-worker.yml

    Deploy, then confirm

    The notification Worker deploys with Wrangler and the job then re-requests it and fails if it is not answering. A deploy step that exits zero means the upload worked, not that the thing is up.

    Green is not the same as live.

  5. 05 \u00b7 boltonvouched — ci.yml

    Testing the database as a database

    The job starts a real Supabase, applies every migration, and runs 147 pgTAP assertions against an empty register before any application code is involved — then reloads fixtures and drives the same build in a browser with Playwright. The scoring suite asserts the no-score floor and the shrinkage arithmetic directly in SQL.

    The rules worth testing are the ones in the schema. Reaching them through a web page tests the web page.

  6. 06 \u00b7 ysbdesignsportfolio — deploy.yml

    A build that fails rather than silently breaking the domain

    This site publishes to GitHub Pages from Actions. The workflow fails the build if CNAME is missing from the output, because a build that drops it takes the custom domain down while still reporting success.

    The failure mode worth guarding is the one that reports success.

Data and access

Multi-tenant Postgres on Supabase, with the rules in the database rather than in the interface.

One database, not one per customer

MasjidOne runs every masjid on a single Postgres. Every table carries a masjid_id that is not null with no default, and every write function fills it from current_masjid(). Row Level Security and SECURITY DEFINER functions scope every read and write. A database per customer would have meant a separate migration, key set and auth setup each time, and the thing the product does happens inside one masjid rather than between them — so the separation would have bought nothing and cost a great deal to run.

Denial by default, as a design rather than a policy

The imam’s advice table has RLS enabled and no policies at all, with every grant revoked. No policy means no row passes; the only way in is through functions that check the role themselves. The check deliberately does not fall back to administrator, because somebody writing to the imam in confidence is not writing to the committee.

Gates in the database, not in the page

A support console reaches real tenants. The anonymous role holds no EXECUTE; the platform-admin check requires is_aal2(), a completed second factor; and entering a masjid you do not belong to writes a support-access row into that masjid’s own audit trail. Editing the JavaScript in a browser gets you nothing, because the rail hiding a row is not a lock.

Schema drift caught by the schema

A health_check() function asserts invariants the code cannot be trusted to hold — including that every table carries its tenant column. It caught a table built exactly as its design drawing showed, where the drawing had no masjid_id at all. 166 migrations are applied and tracked by hand, and a migration that rebuilds a function prints the function rather than pointing at history.

Consent as a constraint, not a checkbox

A directory listing somebody who never agreed to be listed is the failure mode that matters, so it is made unreachable rather than discouraged. A provider row cannot reach live at all unless consent is recorded: it is a CHECK constraint, not a rule in the admin screen. Stored beside the timestamp is a SHA-256 of the exact wording that was on the page they accepted — so what was agreed to is provable later, not just when.

Closed by default, including tables that do not exist yet

The anonymous role is stripped of every grant, and default privileges are revoked too, so a table added next year starts closed instead of waiting for someone to remember. One view is the only door. Making that view readable anonymously turned on a detail worth knowing: Postgres checks function permissions against the calling role even inside a view, so its helpers live in a schema the public cannot reach — which works because EXECUTE is checked at run time while schema access is only checked when a name is looked up.

A certificate that expires takes the listing down on its own

Gas work and driving instruction are gated on both sides. A trigger refuses to file the category without an in-date Gas Safe or DVSA check, and the public view independently drops it the day that check lapses. Neither depends on a person running a report, which is the only version of a compliance check that survives being forgotten about.

What it runs on

Static-first by default, with compute only where something has to happen server-side. Nothing here is on a box I have to patch.

Hosting
GitHub Pages from Actions, custom domains, DNS at one.com — Astro and Next.js to static output wherever the page allows it. Where it does not, Railway runs the Next.js server: Bolton Vouched needs a request-time route so a phone number can be looked up without ever being sent to the browser.
Edge
Cloudflare Workers behind Wrangler for the jobs a static site cannot do — notification fan-out, form intake. Secrets as Wrangler secrets, never in the repository.
Data
Supabase Postgres with RLS, SECURITY DEFINER functions, scheduled jobs and edge functions. 166 tracked migrations on the largest of them.
Payments
Stripe — Checkout, deposits with a held slot, webhooks, and Bacs Direct Debit for recurring billing. Webhook handlers read both of the shapes Stripe has used for invoice fields.
Messaging
OneSignal web push, fired automatically off a published timetable. Resend for transactional mail, behind a Worker rather than a form company.
Clients
PWAs with service workers and offline behaviour, and a React Native app on Google Play — which replaced the Trusted Web Activity it started as, because a webview cannot keep audio playing with the phone locked.

Not AWS, Azure or GCP. The work above is Cloudflare, Supabase, GitHub Actions, Railway and Render. The concepts move \u2014 tenancy, least privilege, build gating, webhook idempotency, deploy verification \u2014 but I would rather say what I have run than imply a console I have not.

Things that broke, and what changed

The ones worth reading are where the check itself was the problem.

Five green builds that had never run

The iOS job built -configuration Debug, which does not embed the JavaScript bundle — it expects a Metro dev server no CI runner has. So the app launched, stayed alive, wrote no crash report, and executed none of the project’s code. Five runs went green. The screenshots were 72% black with a red error banner and nothing was looking at them. Building Release surfaced a real crash within one run.

A check that cannot fail is worse than no check, because it buys confidence. The fix was the device gate above, plus making the job look at the screenshot it already took.

A reconciler that threw every time it had work

A scheduled job wrote an audit row with a null tenant id into a not null column, after its loop — so the first item it resolved raised 23502 and rolled back everything it had just written. It had been green every five minutes for weeks purely because nothing was ever queued for it.

An idle scheduled job is not a passing one. Its check now runs the function against all three answers the upstream can give.

Zero rows read as "not built"

Features were tagged in development across a product site because their tables were empty. They were complete and enforcing; nobody had used them yet. Months of telling customers a shipped feature was coming.

Ask whether the functions exist and enforce, not whether rows do. Emptiness is a usage fact, not a capability fact.

Want the detail?

Every repository above is public and the commit messages explain the reasoning, not just the change. Happy to walk through any of it.