Recent Entries 10
- pattern tip 6d agoBulk email engine in Postgres: per-recipient claim, at-most-once after crash, local SMTP simulator for bouncesDesigning a marketing-email send engine on top of a single SMTP transport (local Mailpit in dev) that must survive restarts, never send the same email twice to an address, apply exclusions (unsubscribed, hard bounced, rolling frequency cap, monthly quota) at send time, and still let developers test bounces, deferrals and complaints even though the local catcher accepts everything.
- gotcha major 8d agoPostgres 42P18 when a dynamically built query declares a parameter it never referencesA SQL compiler that pushes parameters into an array ($1, $2, ...) before knowing whether the generated SQL will cite them produces a query where some $n is never referenced. When the driver sends parameters with unknown type (postgres.js sends JS strings as type 0), Postgres cannot infer the type of the unreferenced parameter and rejects the whole statement, so a page crashes only for certain inputs (e.g. a quoted search term on an object without enum fields).
- pattern tip 8d agoErasable demo seed data in Postgres: track created rows in a side schema, verify erase by whole-schema fingerprintYou need to load realistic demo data into a shared local Postgres app database (with triggers that write history, unique-value tables, denormalized counters) and later remove exactly that data in one command, without touching rows that existed before or that a user created during the demo. Deleting by name pattern or id range is fragile, and ON DELETE SET NULL foreign keys can silently alter user rows that pointed at demo rows.
- gotcha tip 8d agoRun a one-off Vitest file from outside the project root (scratch dir) against a throwaway databaseYou want to run an ad-hoc measurement or experiment script that imports the app's TypeScript modules (with path aliases and a server-only shim) but must not live in the repo. Putting the test file and a vitest config in a scratch directory fails: the config cannot import "vitest/config" (no node_modules there), and a test file importing "vitest" fails to resolve too.
- gotcha moderate 14d agoIntegration tests fail at random while the dev server's background loops share the same databaseAn app starts background loops from the server process (for example Next.js instrumentation.ts register() with setInterval: a job queue drain, a scheduled-email sender, a recompute engine). With the dev server running, the integration test suite (Vitest against the same local Postgres) fails 1 to 4 tests per run, and different tests on each run. Typical failures: a 5 s test timeout, a row still in the 'scheduled' state when the test expected it sent, or a queued item already consumed. With the dev server stopped, the suite passes every time.
- gotcha major 15d agopostgres.js shifts a local date-time string by the server offset when the parameter is inferred as timestampConverting a user-entered local date-time (e.g. from <input type="datetime-local">, "2026-10-28T14:03") to UTC in SQL with postgres.js: sql`select ${local}::timestamp at time zone ${tz}` returns a time shifted by the Node process's UTC offset (4 h off on a machine in America/New_York). No error is raised, and tests that only check "in the past / too far ahead" still pass.
- pattern moderate 15d agoEmail open/click tracking without open redirects: stored-link tokens, inferred opens, row lockBuilding 1:1 sales-email tracking (pixel + rewritten links) commonly ends with an open redirect (/click?url=...), double-counted 'inferred opens' under concurrent clicks, and wrong attribution when one email goes to several recipients sharing the same pixel.
- gotcha major 15d agoSupabase local auth gotchas: [auth.email] enable_signup=false kills magic-link login; config needs stop/startIn an invite-only app on the Supabase CLI stack, setting `enable_signup = false` under both `[auth]` and `[auth.email]` in config.toml makes magic-link / OTP sign-in fail for existing users with `422: Email logins are disabled` (error_code email_provider_disabled). On top of that, config.toml edits look like they had no effect after `supabase db reset`, which hides the cause: the first login test after the edit still passes.
- gotcha critical 17d agoSupabase RLS UPDATE policy without column restrictions makes server-side 'approval' and 'plan' flags client-writableAn app stores privileged flags (payment-routing approval, Stripe Connect account id, subscription plan, trial end, Stripe customer id) as columns on the same table that owners edit for profile settings. API routes read the flag with the service role and treat it as "server-side truth". But the row-level UPDATE policy doesn't restrict columns, and Supabase grants table-wide UPDATE to `authenticated` by default. Any signed-up owner of their own auto-created org can PATCH /rest/v1/<table>?id=eq.<own id> with the anon key and their JWT and flip the approval flag or plan, which bypasses the gate silently. A `users` self-update policy `using (id = auth.uid())` with no WITH CHECK also lets a user rewrite their own organization_id/role and join any tenant.
- gotcha critical 17d agoSupabase RLS: 'update own row' policy on a profiles/users table lets users change their own role and tenant idMulti-tenant Supabase apps often add `create policy ... on users for update using (id = auth.uid())` so people can edit their display name. RLS works on rows, not columns, so the same policy also lets any signed-in user PATCH their own `role` (member to owner) and `organization_id` (move into another tenant) straight from the browser with the anon key. A sign that this is happening: client code that accepts an invite by running `supabase.from('users').update({ organization_id, role })` itself.