Skip to main content

Command Palette

Search for a command to run...

The six patterns behind shipping a React Native app in days

A technical breakdown of what Applighter templates ship pre-wired, and why it compresses TestFlight time by 4x

Updated
9 min readView as Markdown

title: "The six patterns behind shipping a React Native app in days" subtitle: "A technical breakdown of what Applighter templates ship pre-wired, and why it compresses TestFlight time by 4x" slug: why-applighter-customers-ship-in-days-not-months tags: react-native, expo, supabase, typescript, mobile-development cover: https://images.unsplash.com/photo-1555066931-4365d14bab8c?auto=format&fit=crop&w=1600&q=80 canonicalUrl: https://www.applighter.com/blog/why-applighter-customers-ship-in-days-not-months seoTitle: "6 Patterns That Cut React Native Ship Time to Days" seoDescription: "The six architectural patterns — theming, RLS migrations, Stripe entitlements, EAS config — that cut React Native TestFlight time from weeks to days."

I want to open with a specific claim, then defend it: most React Native projects lose their first month to setup, not to product. And I want to show you the exact patterns that dissolve that first month.

I'm going to reference real code paths from the Applighter template codebase. If you're reading this and building your own template, or evaluating templates to buy, use this as a checklist. If a template doesn't have some version of the six patterns below, it's going to cost you time later that you're not accounting for now.

The one-paragraph problem statement

Every React Native project needs auth, DB, RLS, payments, AI, deploy config, and a design system before it can ship a product feature to a user. If you build these yourself, it's roughly four weeks of work — assuming you know the terrain. If you buy a template that has them, it's an afternoon of configuration. The templates that actually deliver this outcome (and most don't) share a small set of architectural patterns. Here they are.

Pattern 1: A theming system that pivots on one hex value

Every UI element in an Applighter template reads its color from a single palette derived from one brand color. The code lives in app/apps/lib/theme.ts:

export function applyProductTheme(brandHex: string) {
  const [h, s, l] = hexToHsl(brandHex)
  const palette = {
    "--background": `${h} ${clamp(s, 0, 15)}% 98%`,
    "--foreground": `${h} ${clamp(s, 0, 25)}% 8%`,
    "--primary": `${h} ${s}% ${l}%`,
    "--primary-foreground": pickContrast(h, s, l),
    "--secondary": `${h} ${Math.max(s - 30, 0)}% 92%`,
    "--muted": `${h} ${Math.max(s - 40, 0)}% 95%`,
    "--accent": `${h} ${s}% ${clamp(l + 8, 0, 92)}%`,
    "--destructive": `0 84% 60%`,
    // …
  }
  return palette
}

The mechanism is standard shadcn-style CSS variables that NativeWind classes read from. What matters isn't that it exists — plenty of design systems have this — but that every screen in every template already consumes the palette. There is no component in the codebase with a hardcoded #4F46E5. A customer rebranding to their color literally changes one string.

If you're building your own template and you skip this, you're accepting that every customer will search-replace colors across a hundred files. That's not a template. That's a starter kit.

Pattern 2: A data provider that gracefully degrades

The data layer in app/apps/lib/data/provider.ts follows a hybrid pattern: attempt the remote fetch, fall back to a bundled static config.

export async function getProductData(slug: string): Promise<ProductData> {
  try {
    const remote = await supabase
      .from("products")
      .select("*")
      .eq("slug", slug)
      .single()
    if (remote.data) return normalize(remote.data)
  } catch (err) {
    console.warn("[provider] remote fetch failed, using bundled fallback", err)
  }
  return staticConfig[slug]  // shipped in the bundle
}

Why this matters for shipping speed: it means the first npx expo start on a fresh clone produces a working app on the simulator without any environment variables set. First-run cognitive load drops from "read 40 pages of setup docs" to "look at the screens."

Nothing kills momentum on a template purchase like a 90-minute env-var setup before you see anything. This pattern eliminates that. Configure the backend when you're motivated, not before you can look at the app.

Pattern 3: RLS policies as first-class migration content

Open supabase/migrations/. You'll see files like:

  • 20260112085737_create_transactions_table.sql

  • 20260129124558_add_primary_image_products_table.sql

  • 20260309120000_enable_rls_support_requests.sql

  • 20260414120000_add_core_features_to_products.sql

  • 20260515120000_create_template_screens_table.sql

Every table creation is paired with the RLS policy in the same or adjacent migration. Nothing lands in production without a policy.

alter table transactions enable row level security;

create policy "users read own transactions"
  on transactions for select
  using (auth.uid() = user_id);

create policy "users insert own transactions"
  on transactions for insert
  with check (auth.uid() = user_id);

This matters because RLS is where indie developers get bitten. It's easy to design a schema, wire the app, and never write the policy — everything works because you're testing as an authenticated user querying your own row. The bug ships when a stranger authenticates and queries your users table.

Applighter templates make the policy a compile-time concern by shipping the migration. You audit it. You extend it. You don't invent it under launch pressure. The Supabase RLS docs are the reference, but you don't need to internalize them Day 1.

Pattern 4: A Stripe → entitlement → download pipeline that works

This is the one that saves the most raw hours. The Applighter code at app/api/webhook/stripe/route.ts:

export async function POST(req: Request) {
  const sig = req.headers.get("stripe-signature")!
  const body = await req.text()

  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(body, sig, WEBHOOK_SECRET)
  } catch (err) {
    return new Response(`webhook signature error: ${err}`, { status: 400 })
  }

  if (event.type === "checkout.session.completed") {
    const session = event.data.object as Stripe.Checkout.Session
    const { userId, productId, licenseType } = session.metadata!

    const { isBundle, targets } = await resolveGrantTargets(supabase, productId)
    await grantProducts(
      supabase,
      { userId, productId, licenseType, source: "stripe_purchase" },
      targets
    )

    await sendReceiptEmail({
      userId,
      downloadUrl: `${SITE_URL}/dashboard`,
      licenseType,
    })
  }

  return new Response("ok")
}

The paired download handler at app/api/download/[productId]/route.ts validates entitlement on every request — either via the authenticated session, or via a Stripe session_id for guest purchases. It fetches the repo owner/name/branch from the product row and streams a ZIP of the source.

Rebuild this yourself and you'll spend three to five days on: webhook signature verification, idempotency keys so retries don't double-grant, bundle expansion, license-type modeling, guest checkout flow, download authorization. It's not conceptually hard. It's just a lot of small correct decisions in a row. The template makes them all for you.

Read the Stripe webhooks docs if you want to understand what the code is doing — but the code is already written.

Pattern 5: EAS build configured, not documented

There's a slash command called /ship in the docs. It runs:

eas build --profile production --platform all --non-interactive
eas submit --profile production --platform all --non-interactive

This works because eas.json is committed, app.json has the bundle identifiers filled in via a config script, and the submission credentials are wired to environment variables — not code changes. Store metadata (privacy policy URL, support URL, category, keywords) is templated.

If you've never done an Expo EAS build, this saves you a full day of ping-ponging between the Apple Developer portal, expo.dev, and Xcode. The templates ship the config that most tutorials leave as an exercise. Combined with Expo Router, the "how do I structure this app for deploy" question is answered before you clone.

Pattern 6: Codebase legibility for AI agents

The newest pattern, and the one that will matter more every quarter: Applighter templates are built to be extended by AI agents (Claude Code, Cursor, Copilot). The .claude/ directory in each template contains slash commands like:

  • /add-screen — scaffolds a new screen with routing, styling, and data hook conventions

  • /swap-backend — replaces Supabase with an alternative that implements the same interface

  • /audit-security — runs a security review over auth flows, RLS, and API routes

They work because the codebase is agent-legible: small files with obvious responsibilities, NativeWind classes with Tailwind semantics, typed data providers, colocated components. An agent extending this codebase produces diffs that pass review because there's nowhere weird to wander.

A codebase from 2022 with class components, StyleSheet objects, and a utils/ directory full of god-files is not agent-legible. Agents can technically edit it, but the diffs are chaos.

Comparison table

From scratch Free boilerplate Applighter
Auth (email + OAuth) 3–5 days 1–2 days Included
DB + RLS 2–4 days Not included Included
Payments + entitlements 3–5 days Not included Included
AI streaming 4–7 days Not included Included
EAS build/submit 1 day 0.5 day Included
Push notifications 2 days Not included Included
Time to TestFlight 4–6 weeks 2–3 weeks 3–5 days
Ongoing updates You maintain Depends Lifetime
Price $8–20k dev time Free + hidden time $79 one-time

Compare against Instamobile's template catalog for market context.

The bimodal shipping distribution

We track days from Stripe receipt to first TestFlight upload. It's not normal — it's bimodal:

  • Mode 1: 3–5 days. Rebrand-and-launch on an existing niche template.

  • Mode 2: 2–4 weeks. Novel product built on template infrastructure — usually AI.

The ratio matters more than the raw numbers: whether you're doing a two-day rebrand or a four-week AI product, the template compresses the ground-up equivalent by roughly 4x.

Where the pattern breaks down

Skip a template if:

  1. Your data model doesn't fit — game engines, real-time collaborative editors.

  2. You're learning React Native and want to walk every step.

  3. You need a stack we don't ship — Firebase, Amplify, Convex, bare RN.

Otherwise, the math is one-sided.

FAQ

Q: What does "ship in days" mean? 3–5 days for a rebrand-and-launch. 2–4 weeks for a novel product on top of the template.

Q: Do I need Supabase knowledge to start? No. Templates run with the DB seeded and the RLS policies applied.

Q: What if a new Expo SDK ships? Template updates land in your dashboard. Pull, diff, ship.

Q: License terms? $79 one-time, lifetime updates, ship as many apps as your license type allows. 7-day refund window.

Where to look next

Browse the Applighter template catalog. Start with the Weather App if you want to see the patterns in the simplest possible template, or AI Voice Notes if you want to see how AI wiring lands on top of the base.