Entitlements Need Plan and Status
Checking only plan name or only status made the same user Pro in one feature and Free in another. isProEntitled(plan, status) is the fix.
Engineering write-ups on how Storyie is built — architecture, tooling, and code.
Checking only plan name or only status made the same user Pro in one feature and Free in another. isProEntitled(plan, status) is the fix.
Soft delete on the server is only half the story. The sync pull contract must carry tombstones as typed delete changes — not omit the row and hope clients notice.
A notification tap is three different races depending on app state. Storyie stopped guessing with setTimeout and waits for Expo Router readiness instead.
In-memory retry timers forget their schedule when the OS kills the app. Storyie stores the backoff deadline in SQLite so the next launch picks up exactly where the last attempt left off.
Auth is expensive. We put a pluggable sliding-window rate limiter ahead of session lookup so brute-force never pays for a DB round-trip — and the Expo client uses the same budget.
One auth user is not enough. Storyie needs a public profile and a private profile on every real signup. We put that invariant in a Better Auth hook — and made every other path fail closed instead of inventing a row.
An offline edit is a promise the app has to keep after a force-quit. Storyie persists every pending mutation as a SQLite row with a status state machine, exponential-backoff retry, and an ownership guard against stale server data.
The canonical Lexical-to-Markdown path needs a live editor and a registered node for every type. Our export runs in a Lambda, so it walks the JSON tree directly instead.
A hard DELETE in a local-first app is a bug waiting for the next sync. Here is the tombstone column and the two guards that keep a deleted row deleted.
Our free plan allows 31 diaries a month. When "a month" was UTC in one place and local time in another, the reset and the dedup key disagreed at midnight on the 1st. Here is the fix.
A guest writes diaries, then signs in with Google. Better Auth deletes the anonymous user right after the link resolves — so one hook has one chance to move the data. We made it atomic, retryable, and safe when both sides already exist.
LWW is not "give up and take the newest timestamp." Storyie forgives one second of clock drift, breaks ties toward the server, guards uncommitted local edits at the SQLite layer, and always tells you when your version was replaced.
Web login broke with state_security_mismatch while mobile kept working. The fix was one plugin, in one position: nextCookies() had to be last.
Migration scripts exit 0 when they do not crash, not when your data arrived intact. We wrote a separate, read-only verification step to actually prove it before switching traffic to Turso.
Debounce is a data-loss window. Storyie writes to on-device SQLite synchronously on every keystroke and defers the network save — then uses refs, not state, to keep the race honest.
A 44-post import turned an O(N) parse into an O(N²) build and blew past the 60s SSG timeout. The fix hinges on caching the parse Promise, not its result.
Our FK cascades look correct in the schema and do nothing at runtime. We delete a user across 25 tables explicitly, inside one transaction, by hand.
A provider swap touches every page that reads the current user. We contained the blast radius to a single file — a CompatUser shim — instead of a 56-file rewrite. The trade-off: the shim tells one deliberate lie.
Per-route auth is a forgetting machine. We rebuilt Storyie’s Hono API so a new route is locked unless you explicitly open it — and a regression test keeps it that way.
We run Claude inside GitHub Actions to draft X posts weekly, store them in Supabase as drafts, review them ourselves, then let a second workflow publish on schedule. Here is how the whole thing fits together.
Repeated price changes, per-environment duplication, and manual SQL migrations convinced us to declare Stripe resources in Terraform. Here's the full setup.
We run RLS policies on every table in Storyie. After designing and operating 27 of them, the patterns collapsed into seven reusable shapes — here is each one with its intent and the pitfalls we hit.
Relying only on WHERE clauses for tenant isolation is an accident waiting to happen. Here is how we moved that guarantee down to the database with Supabase RLS.
Plan limits that live in a pure-function package mean one change propagates to web, mobile, and tests simultaneously — no copy-paste, no drift.
Storyie is a cross-platform diary app where every entry is private by default, and going public is one tap. Here's what it does and how it's built.
One sst.config.ts, two stages, no WAF bill — how we wired SST v3 and CloudFront to keep production safe while still being able to test Stripe webhooks and OAuth flows.
Six months of running Storyie on SST v3 produced a list of things the docs don't cover — Pulumi Output resolution in transforms, stage-scoped env loading, Cron offsets to avoid connection pool contention, and more.
We wired an AI agent to the App Store Connect CLI so that keyword research, 10-locale metadata drafts, and monthly ASO reviews run mostly unattended — with a dry-run gate before anything lands.
We added llms.txt and Markdown route handlers to Storyie so AI assistants can understand the app without parsing HTML — dynamically generated, ISR-cached, and privacy-safe by default.
Same architecture, different implementations: how we built one image upload pipeline for Next.js and Expo on top of Cloudflare R2.
Supporting 10 languages in a cross-platform monorepo means four different i18n strategies — one per layer. Here is how we wired them together without losing our minds.
After a year of shipping Storyie, our migration file count hit 56. Here is what we learned about running Drizzle ORM in production with Supabase — two-stage CI deployment, when to write custom SQL, and how we think about rollbacks.
Write an issue, attach a label, and let Claude handle the plan-then-implement loop — with a required human checkpoint between the two.
Supabase Auth, RLS, and RBAC each solve a distinct problem. Here is how we wired all three together in Storyie so the database enforces security even when application code forgets to ask.
One endpoint, six diary patterns, ten languages — and a validation + fallback layer that keeps the feature working even when the AI API is down.
How we wired SST Cron, Lambda, and the Anthropic API to generate a monthly diary analysis for every Pro user — and the design decisions around input limits, structured output, and idempotency that made it production-safe.
Building a Lexical editor that works identically on web and mobile is one of the most challenging aspects of cross-platform development. At Storyie, diary entries created on our Next.js web app must render perfectly in our React Native mobile app, and vice versa. Through a two-package architecture...