Blitz Kit
Get started

Changelog

Every notable change to the product, newest first.

  1. Unreleased

    Fixed

    • The multi-tenancy module was not actually removable, despite three years of markers saying so. Uninstalling it left five compile errors: a let declared inside its own seam block so the function's return statement referred to nothing, a JSX prop and an object shorthand outside theirs, a team_members quota key in an exhaustive Record<QuotaKey, …> that was never seamed at all, and a test factory missing from the module's footprint. The static enforcement test had been passing throughout and could not have caught any of them — it greps, and grep cannot know that deleting a resolver breaks an exhaustive record. pnpm delivery:verify found all five on its first run.
    • Optional-module tables no longer leak into the core Drizzle schema barrel. drizzle.config.ts points at that barrel, so a table listed there appeared in pnpm db:generate diffs on installs that never received the module's migration. WhatsApp's three tables and the SARS layer's four now live behind @/drizzle/schema/whatsapp and @/drizzle/schema/tax, matching how the teams module has always done it. The SARS case was the worse of the two: its migration was already excluded from lower tiers while the schema declaring its tables was not.
    • Merchant-of-record invoices no longer store a South African VAT split. Every invoice was written with a hardcoded 15% split regardless of currency or provider, so a USD sale carried a VAT figure for a tax that was charged by the merchant of record and never remitted by you. The write path now resolves the VAT treatment through the same function the billing portal uses to decide what to display, so the stored row and the rendered invoice cannot disagree.
    • `renderTaxDocument` refuses to issue an SA tax document for a merchant-of-record supply rather than printing a 15% VAT line on it. There is no correct document to produce — the buyer's tax receipt comes from the MoR — so it throws and names the guide.
    • `invoices.csv` in the year-end bundle now lists only paid invoices, matching the VAT201 working and the summary beside it. A refunded invoice used to appear in the detail file and in neither of the other two, so the bundle did not tie to itself.
    • The 12-month VAT-threshold window no longer shifts on a leap day. Going back twelve months from 29 February asked for a date that does not exist and rolled forward, starting the statutory window a day late once every four years. It now clamps to the end of the target month.

    Changed

    • The `whatsapp-notifications` feature flag now actually gates WhatsApp. It existed but was consulted by nothing — a kill switch that did not kill. The notification router checks it before the consent gate and falls back to email, reporting whatsapp_disabled distinctly from no_whatsapp_permission, and the notification settings page stops offering a channel that cannot send. It ships enabled: the channel opt-in is what stops an unconfigured install from sending, so defaulting the switch to off would only have disabled a working channel on upgrade.

    Added

    • `pnpm delivery:verify` — a release gate that compiles a stripped delivery repository. ADR 005 requires each licence tier's generated repository to typecheck and build before publication, and nothing enforced it; delivery:build printed "not yet verified" and that sentence was the gate. The new script removes an entitlement's files, deletes the marked seam blocks, and runs tsc --noEmit and next build on the result — so it performs the documented uninstall rather than a subset of it, which puts the seam-marker convention itself under test.
    • WhatsApp and the SARS/VAT helper are now optional modules (config/modules.ts), like multi-tenancy. The registry gained a ModuleKind discriminator because they are optional for different reasons: teams ships to every buyer and is switched off with an env toggle, while the other two are licence entitlements that lower tiers never receive. Fifteen files had a hard dependency on WhatsApp — including the notifications router, both POPIA account primitives and the schema barrel — and four on the SARS layer, so those tiers could not have compiled. All three modules now typecheck and build with their files and seam blocks removed.
    • A WhatsApp channel port for the notifications router. lib/notifications now depends on lib/notifications/whatsapp-channel.ts and reaches the transport through a single seam, so the router ships to tiers that have no WhatsApp module. The send capability got stronger rather than weaker: instead of naming a symbol-boxed WhatsAppRecipient it cannot construct, the router receives a send closure from the adapter, so there is no type to satisfy at all.
    • A public roadmap and a maintenance commitment, at docs/roadmap.md and on the site at /roadmap. Three buckets, and the line between the first two is a rule rather than a mood: _committed_ means a licence tier already sells it, so it is a debt rather than a plan. There are no dates anywhere on it — what replaces them is the public changelog and build log, which you can check before you buy and after. The PayFast and Peach v1.1 promises now have a public page that makes them, and their guides link to it.
    • A support policy (docs/support-policy.md) that separates a defect in the kit — mine to fix at every tier, always — from help with your own product, which is where the tiers differ. Its per-tier table derives from the entitlement ladder, so it cannot promise something the licence does not grant. It publishes no response-time target, and says why.
    • A retention rule for the waitlist, and a sweep that enforces it. waitlist was the only personal-data store in the kit with no end date: addresses were held indefinitely. Entries that never converted are now erased after 730 days, and tombstoned ones after the account grace window; converted entries are never touched, because account retention governs them and the referral graph depends on the row. The register states plainly that the period is a stated policy rather than a statutory one.
    • Waitlist capture surfaces, so one address can be on more than one list. waitlist.sources is an ordered set rather than a scalar, so someone who joins a free-course list and later asks for product access lands on both instead of being silently moved between them. POST /api/waitlist rejects an unrecognised source rather than defaulting it, and the admin CSV export carries the column. Migration 0017, with its down pair.
    • The complete repository documentation set. The README now explains installation, operation and the cumulative kit tiers; ARCHITECTURE.md maps every cross-cutting seam and indexes the ADRs; SECURITY.md defines private disclosure and the release-security gate; and CONTRIBUTING.md carries explicit code and translation contribution terms.
    • A completed upstream-merge upgrade guide and feature teaching-layer lint. The guide documents inspection, conflict resolution, rollback and database precautions against a proven customised fork scenario. CI now refuses a feature directory without substantive WHY.md and PROMPT.md companions.
    • Deployment guides grounded in the infrastructure the kit actually ships. The standard route is xneelo Cloud Johannesburg with self-managed PostgreSQL, plus Afrihost, xneelo dedicated, Vercel + Neon EU and generic-VPS alternatives. DNS/TLS and backup restore drills are separate, checkable runbooks rather than one-click claims.
    • A generated residency matrix and ZAR deployment-cost calculator. The matrix renders every processor, justification and swap path from config/residency.ts; the cost guide renders from a dated price catalogue. Exact-output tests fail if either generated document drifts from config, and foreign costs require an operator-supplied exchange rate rather than a stale embedded one.
    • Legal pages as code — a privacy policy and terms of service at /privacy and /terms, with a reusable review-date stamp and "starting point, not legal advice" caveat that prompts 30–32 reuse. Every document is registered in config/legal.ts.
    • A privacy policy that cannot drift from the stack. The sub-processor table, the data categories, the retention periods and the cookie list all render from config/residency.ts, config/retention.ts and lib/analytics.ts — change a register and the published policy changes with it, with no edit to the page. A test proves it by substituting the residency config and asserting the policy follows.
    • POPIA-shaped cookie consent (components/cookie-consent.tsx), scoped to the trackers the deployment actually runs. Cookieless analytics are disclosed but not gated, PostHog is consent-gated, and session replay is a second, separate opt-in. The banner does not render at all when there is nothing to ask about.
    • Form 4 direct-marketing consent (components/form4-consent.tsx) on /settings/notifications, writing to the existing marketing_consent store. Grant and withdrawal are both audited, and the admin blast re-derives consent at send time, so unticking the box removes you from a campaign already in flight.
    • docs/legal.md, including the list of omitted citations and unresolved questions to hand to an attorney before publishing.
    • A privacy page for the person, not the lawyer/settings/privacy puts every right in one place: download everything we hold, ask for a correction, or delete the account. It sits on the existing export and deletion plumbing rather than duplicating it.
    • Data-subject request lifecycle with tracked due dates and an admin fulfilment queue at /admin/dsar, sorted by deadline with the overdue count stated up front, plus a daily reminder job. Requests are always tied to an authenticated session, and fulfilling one produces a _timestamp_, never an export — the data only ever travels to the data subject's own signed-in browser.
    • Corrections that cannot silently rewrite a record. An approved change captures the value it overwrote in the same transaction, and the audit row carries the before _and_ the after. Correcting a date of birth re-runs the 18+ check and refuses to open the age gate sideways.
    • Account deletion behind a typed confirmation, with what is destroyed and what survives (and why — SARS keeps invoices for five years) shown before the field, not behind it.
    • The age gate now runs at signup, server-side, in the one hook every account-creation path passes through. An under-18 date creates no account and is not retained. Collecting a date of birth is opt-in (AGE_GATE_AT_SIGNUP) because most products should not be asking.
    • Pluggable file storage with two S3-compatible adapters — Cloudflare R2 (default, zero egress) and Hetzner Object Storage (documented swap) — sharing one SigV4 signer, so the two are genuinely interchangeable rather than parallel implementations.
    • Direct-to-bucket uploads via pre-signed URLs, with MIME allowlisting, a hard per-object size ceiling, and per-plan storage quotas enforced through hasQuota().
    • Scheduled orphaned-object cleanup, with a minimum-age threshold so an in-flight upload can never be mistaken for an orphan.
    • Public changelog page at /changelog, rendered from this file.

    Changed

    • The storage_mb quota is now backed by real usage instead of a zero stub. It is measured as a stock rather than a monthly flow, so deleting a file frees the allowance immediately.
    • Data-residency register now lists both storage providers with their true jurisdictions. Hetzner Object Storage is recorded as EU, not South African: it runs in Falkenstein, Nuremberg and Helsinki, and there is no Cape Town object-storage region. The previous documentation implied files would sit beside the Cape Town compute; that has been corrected.
  2. 0.5.0

    Added

    • Two-sided referral programme paying out on the referred user's activation rather than at signup, with abuse checks re-run at payout time.
    • Pre-launch waitlist with referral-to-skip mechanics and live position ranking.
  3. 0.4.0

    Added

    • Resumable, server-driven onboarding wizard with abandonment re-prompts.
    • Marketing site: landing page, blog, RSS feed, and SEO metadata with hreflang alternates.
  4. 0.3.0

    Added

    • Payment abstraction over Paystack, Yoco, Ozow and a merchant-of-record provider, with webhook verification, idempotency and dunning.
    • Per-plan quota gating driven entirely by config/plans.ts.
  5. 0.2.0

    Added

    • POPIA groundwork: typed data-residency register, centralised retention policy, audit trail, account deletion with a grace window, and the sub-processor disclosure UI.
  6. 0.1.0

    Added

    • Initial release: authentication, multi-locale support (EN/AF, with ZU/XH stubs), theming, background jobs, and the database layer.