iziwerk — the product
A freelancer workspace with a harder problem underneath: keeping each client’s data separate.
No longer actively developed. Kept here as a portfolio record of the design, engineering and decisions behind the build.
One tablepublic.invoicesboth tenants
signed in as C1, a client on Project One · own sign-in, never the admin key
sbC1.from('invoices')
{ data: [], error: null }
C1 cannot read P2 invoices
sbC1.from('invoices')
{ data: [], error: null }
C1 cannot read P1 DRAFT invoices
Same product, the hard half: a secure, multi-tenant SaaS where a freelancer runs every client — proposals, invoices, files, messages, documents — in one place, plus the other side of that market: a hiring board where a job was posted, applications came in, and a hire landed straight in a shared workspace. Built in deliberate phases, security-first, where every phase had to prove itself before the next was allowed to start — then audited against itself, ruthlessly.
// the spine
Row-Level Security, at the database
The spine of the whole thing is Row-Level Security — confidentiality enforced in the database, not in app code that could carry a bug. A client on one project physically cannot read another client's invoices, files or messages, even with a valid token hitting the API directly. And I didn't take that on faith. Every phase added adversarial isolation tests to one suite — a script that logs in as a hostile tenant and tries to read or change what isn't theirs — alongside checks that the owner still can.
// the suite, phase by phase
One suite, growing with every phase
Each phase added its own hostile checks to the same suite. In each bar, the bright part is what that phase added and the dim part is what it carried. The dates and results are from the build record: the reds, and the one phase whose checks first ran with the next.
| Phase | What it added | Isolation suite | On record |
|---|---|---|---|
| A1 · Foundation | schema + RLS + auth (passwordless email code, mandatory TOTP, one-time recovery codes, passkeys; clients via magic-link, invite-only) | 34 | Written, with a pass bar of 34 / 34. |
| A2 · Workspace | dashboard, one-time client invites, per-project workspace (proposals, invoices, files, messages), tier gating | 69 | 68 / 69 The red: a storage rule that refused everyone, the owner’s own avatar upload included. Fixed the same day in a new migration. |
| File security | server-side file-type allow-list (blocks executables), DB-enforced per-tier storage quotas | 82 | 81 / 82 The red was the test’s own setup, not the rule. Test fixed, re-run 82 / 82. |
| Documents + Support | document engine (17 types × 3 styles × 2 brands, PDF), help centre + ticketing, server-locked admin | ~130 | About 48 checks written. Their first run came with the next phase. |
| Billing + Email | Stripe subscriptions (plan set only by the signature-verified webhook, idempotent, out-of-order-safe), transactional + auth email | 161 | 161 passed, 0 failed, against the migrated database. |
| Marketplace + reputation | a two-sided hiring board (post a job, apply, get hired straight into a shared workspace), public reputation profiles with real client reviews, a referral program, and a second poster-billing dimension | 427 | 427 / 427. |
| GDPR, growth + a full self-audit | account deletion & data export, time tracking, expenses, recurring invoices, CSV import, global search + a ⌘K palette, an admin/moderation console — then a 355-finding adversarial audit of the finished product and a twelve-pass fix program | 556 | 517 / 519 Both reds were a real production bug: every invited client’s join was refused. 556 / 556 against the live database. |
556 / 556 passing — tenant confidentiality holds at the database. It was a suite, not a screenshot: it ran against the live database at go-live and again after the fix program (556/556 on 21 July 2026).
The last run, after the job board was retired: 562 / 562.
// auth
Auth done right, not hand-rolled
Passwordless by design. Freelancers get a mandatory authenticator (TOTP) plus optional passkeys and saved recovery codes; clients get a frictionless magic link with optional TOTP — invite-only, always. The recovery path is built so an email inbox can never be a single master key.
// the decisions
The decisions that separate real from fake
The ones that matter are the unglamorous ones: privilege columns like plan and is_admin that are physically un-writable from the client and only ever set server-side; a white-label flag generated off the subscription tier so it can't be forged; payment status that changes only on a verified webhook, never on a browser redirect; uploads that can't be bypassed with a direct storage call. Every one of those is asserted in the test suite.
// the shared application
Every tenant uses the same app. The database decides what fills it.
Each part of the app, with the checks from the suite that guard it, quoted the way the suite prints them. F1 and F2 are two freelancers, two tenants; C1 and C2 are their clients; P1 and P2 are their projects.
-
01 Proposals
A draft stays editable. Once a proposal is sent its content is locked, and its status only moves through the app’s own actions.
- F1 CAN edit a DRAFT proposal title (grant not over-restricted)
- F1 cannot edit a SENT proposal (edit-lock trigger)
- F1 cannot directly write proposals.status (column-locked -> RPC only)
-
02 Messages
Client and freelancer write in the project’s own thread. A client posts only as themselves, and only into a project they belong to.
- C1 CAN post a message as themselves into P1
- C1 cannot spoof author on a P1 message
- C1 cannot post a message into P2
-
03 Plan and storage
The plan badge and the storage meter. Nobody can write their own plan, and the quota is enforced by the database, not by the upload form.
- F1 cannot self-upgrade profiles.plan (column-locked)
- Taste at 500 MB blocks an over-quota upload (IZ507)
- Werk (25 GB) allows an upload that Taste blocked
-
04 Security
Two-factor sign-in is required and can’t be turned off. The backup codes sit where no signed-in user can reach them, and no account can make itself an admin.
- C1 cannot reach recovery_codes (no grant)
- F1 cannot self-grant is_admin (column-locked)
-
05 Support
A ticket belongs to whoever opened it. Nobody else can read it or answer in it, and nobody can post in someone else’s name.
- C1 cannot read F2 ticket
- F2 cannot read C1 ticket
- C1 cannot spoof author_id on own ticket message
// documents
The document engine
The feature I'm proudest of. One variable-driven system renders every document a freelancer sends — proposals, invoices, quotes, contracts, receipts — in three styles the user picks, in their own branding, exportable to PDF. Free and mid tiers ship a tasteful default; the top tier unlocks full white-label — the same identity system, swapped by a few tokens. Seventeen types, three styles, two brand modes.
06 The editor
A quote, with its style picked, beside the live A4 preview. A sent document is fixed, and a draft stays private to the freelancer.
- F2 cannot read F1 documents
- C1 cannot read P1 DRAFT documents
- F1 cannot edit a SENT document data (edit-lock P0001)
17 × 3 × 2, every one rendered
The same data in every style and both brand modes. Pick a document, then switch its style or brand: only the skin changes. In white-label the amber gives way to the freelancer’s colour, shown here with a sample one, and the iziwerk mark is gone.
Rendered from the document system the editor was built on, with its placeholder sample data: page one of each, at A4.
// the method
Directed, not vibe-coded
Every architectural and product call here is mine, and so is every decision to reject one. I ran an AI-orchestrated pipeline to build against those calls — a planning layer that turned intent into exhaustive build instructions, an implementation layer working in plan-then-review-then-execute cycles — and I sat in the middle red-teaming the plans and running the isolation suite against the database myself. The judgment and the refusal are the scarce parts. When the isolation suite came back 81 of 82, I didn't move on. The one red was the test's own setup, not the database rule; I fixed the test and the re-run came back 82 of 82. Nobody was waiting on that decision but me.
The obvious question is why you'd need me rather than the same tools. This is what the tools produce without a verification layer on top: a privilege column that looks completely plausible and is writable from the client; a payment status that flips on a browser redirect instead of a signature-verified webhook; a storage rule that holds in the UI and not against a direct API call. Every one of those reads fine in review and fails the moment somebody hostile looks at it. Catching them is not a prompt — it's knowing they're the things to go looking for, and refusing to sign off until each one is asserted in a suite that is run again before the next release.
Then I turned the same rigour on my own finished product. A read-only adversarial audit went through the finished product, refute-by-default, and surfaced 355 findings. A twelve-pass fix program followed — double-charge and GDPR-deletion gaps, gameable reputation and referrals, an unguarded delete path, and, tellingly, a broken invite-redemption bug the isolation suite caught the instant it ran again. The few it didn't close were parked and named on the record. After the fix program the suite stood at 556 / 556 against the live database (21 July 2026); its last run, after the job board was retired, was 562 / 562 (1 August 2026). Auditing your own work as ruthlessly as a hostile reviewer would is the difference between "it demos" and "it holds."
// status
Honest status
The product shipped and ran live: Stripe checkout in production, and Terms and Privacy v1.0 in force from 9 July 2026. It is no longer actively developed and stays here as a portfolio record. Both halves were built and shipped on their own infrastructure — the freelancer workspace, and the hiring board that sat on top of it. The claims above aren't aspirational: 556 isolation assertions green against the live database, the 355 audit findings worked through in a twelve-pass fix program. Read this page as a record of what was built rather than a current feature list. The hard, easy-to-get-wrong part — building both sides of a market on one secure spine and having it survive its own audit — was done.
RLS isolation: 562 passed, 0 failed (of 562).
✓ RLS ISOLATION PASSED — tenant confidentiality holds at the database.