Skip to main content

Command Palette

Search for a command to run...

Our React Native Template Launch Playbook (Config-First)

How every Applighter template ships from a single config file: five stages, zero bespoke pages, 90 minutes flat

Updated
10 min readView as Markdown

Every new Applighter template ships the same way. A single *.config.ts file gets checked in, a row lands in a Supabase table, and the catalog page auto-regenerates. There is no bespoke marketing site, no custom checkout wiring, no forgotten launch email. This is our React Native template launch playbook, the one we run every single time, because the alternative (bespoke everything) is the reason most template shops die at three products.

Desk with laptop and code editor open

The 30-second version

The Applighter React Native template launch playbook is a five-stage pipeline: (1) write a single *.config.ts file describing the product, (2) register it in app/apps/config/index.ts, (3) seed Supabase products and product_licenses rows, (4) let /apps/[slug]/page.tsx render the marketing page from that config, and (5) let scripts/blog-automation/generate-blog.sh queue launch content. No per-template pages. No per-template checkout code. Every template ships identically.

Why we made template launches boring

The first three Applighter templates each shipped as a snowflake. A hand-crafted landing page under /apps/fitness-app/, a second one under /apps/weather-app/, a third under /apps/ai-voice-notes/. When we bumped our Stripe license grant logic in month four, we had to edit three checkout call sites. When we changed the "What's included" section copy, we had to touch three JSX files. When a customer asked for a bundle discount across two of them, we had to hard-code the SKU list.

That's the death spiral. Every new template makes the next one 20 minutes harder to ship. By the time you have twelve, adding a thirteenth costs you a full day just in copy-paste tax.

So we rewrote it. Now every template is a data structure, not a page. The page is generic. The checkout is generic. The Slack launch thread is generic. The only thing that's specific to a template is its config file.

Here's the exact playbook.

Stage 1: Write one config file

Every template lives as a single .config.ts file under app/apps/config/. The shape is enforced by the ProductData interface in app/apps/lib/data/types.ts: 50+ typed properties covering everything the marketing page, the checkout, and the launch tooling need.

Concretely: app/apps/config/ai-voice-notes.config.ts is a single 400-line TypeScript object. It contains the product name, tagline, slug, hero copy, feature bullets, the sixteen-screen showcase, the pricing tiers, six FAQs, testimonials, and the tech stack chips. Nothing else exists for that template. No page component, no route handler, no marketing copy in JSX.

Same for app/apps/config/chat-with-pdf.config.ts. Same for app/apps/config/fitness-app.config.ts. Same for every future template. When we ship the next one, we don't open a design file. We open a config file.

The rule: if you find yourself editing anything outside app/apps/config/ to launch a template, stop. You're violating the playbook.

Stage 2: Register it in the index

app/apps/config/index.ts is the template registry. It's a single exported Record<string, ProductData>:

export const appConfigs: Record<string, ProductData> = {
  "weather-app": weatherAppConfig,
  "fitness-app": fitnessAppConfig,
  "e-learning-app": eLearningAppConfig,
  "taxi-booking-app": taxiBookingAppConfig,
  "ai-voice-notes": aiVoiceNotesConfig,
  "chat-with-pdf": chatWithPdfConfig,
};

Adding a new template is one import and one key. That's the entire code-side registration. Nothing else in the app needs to know the template exists.

Why does this matter? Because app/apps/[slug]/page.tsx uses generateStaticParams() to pre-render marketing pages at build time by iterating over Object.keys(appConfigs). The moment you add a key, the page exists. The moment you remove one, the page 404s. The registry is the source of truth.

Stage 3: Seed the database

Applighter's marketing pages are hybrid: they hydrate from the static config (fast, versioned in git) and enrich from Supabase (dynamic pricing, license tiers, purchase-count social proof, screenshot ordering). So a template also needs two rows and change:

  1. One row in products (slug, name, short_description, is_disabled=false)

  2. Rows in product_licenses: one per tier (single, multiple, enterprise), each with a Stripe price ID

  3. Optionally, rows in template_screens for the screen-type breakdown The schema itself lives in migrations we can point at: supabase/migrations/20260112085714_create_products_table.sql, supabase/migrations/20260113100000_create_product_licenses_table.sql, and (because we now sell bundles) supabase/migrations/20260616120000_add_bundle_support.sql.

Row-level security is on. The public catalog reads through the anon client in modules/db/supabaseClient.ts; server-side reads (including the license lookup during checkout) go through the service-role client in modules/db/supabaseServer.ts. Neither of those files ever changes per template. They're generic infrastructure. See Supabase's RLS guide for how the policies are structured.

Database schema visualization on a monitor

Stage 4: Let the pages render themselves

This is the part that makes new templates cheap.

app/apps/[slug]/page.tsx is a single dynamic route that handles every template. It reads the config for the slug, wraps the render tree in <ProductThemeProvider> (from app/apps/components/product-theme-provider.tsx), and dispatches to the generic generic-product.tsx layout. Screenshots, features, testimonials, FAQ accordion, pricing table, checkout buttons: all generic components fed by the config.

The full catalog at app/apps/page.tsx is the same idea: iterate over appConfigs, filter out is_disabled, render a card grid.

Zero per-template JSX. When a new template ships, the marketing pages appear as a byproduct of the config existing.

The checkout flow is equally generic. app/api/checkout/route.ts accepts productId and licenseId, looks up the license in Supabase, creates a Stripe checkout session with the right metadata (userId, productId, licenseId, licenseType, allowedUsers, allowedProjects, plus Meta Pixel and Datafast tracking IDs), and returns the URL. It handles free products, guest checkout, partner referrals, and authenticated buyers, all in one file, none of it per-template. Stripe's checkout session API does the heavy lifting; our route is thin.

Stage 5: Kick off the launch content

The launch itself (the blog post, the Slack thread, the Dev.to and Hashnode cross-posts) is a script.

scripts/blog-automation/generate-blog.sh is a 760-line bash pipeline. It invokes Claude with a topic, parses the five-section output (canonical, Medium, Dev.to, Hashnode, SEO brief) by counting ━━━ delimiters, POSTs the canonical body to app/api/outrank/webhook/route.ts for insertion into the blog_posts table (migration: supabase/migrations/20260113120000_create_blog_posts_table.sql), and drops the platform-specific rewrites into a Slack thread for manual cross-posting.

The runbook for adding a new template's launch content: append two lines to scripts/blog-automation/blog-tracker.md. One for the internal queue (canonical blog on our own domain, dofollow) and one for the external queue (Medium and LinkedIn, engagement-first). The daily cron picks them up. See app/api/outrank/webhook/route.ts for how the payload gets validated and inserted.

Config-first vs bespoke: the actual cost delta

The playbook is opinionated. Here's why we chose it over the alternatives:

Approach New-template cost Marginal cost at template #13 Where you edit
Bespoke page per template ~2 days ~1 day (copy-paste tax) JSX, routes, checkout
Popular template-shop CMS (Instamobile-style) ~4 hours per template ~4 hours (no compounding, but no reuse either) CMS admin
Applighter config-first playbook ~90 minutes ~90 minutes (flat) One .config.ts file

The trade-off is real. Config-first means every marketing page looks like every other Applighter marketing page. That's fine for us. We're selling to indie devs and small teams who care that the templates work, not that each product page is a design snowflake. It would be wrong for a shop selling one flagship product where the landing page is the moat.

Mobile phone showing app screens

The three rules that keep this from rotting

  1. The config is the spec. If a marketing team member wants to change a template's tagline, they open the .config.ts file and change one string. No PR to page.tsx. If they can't do that, we broke the abstraction.

  2. Never add per-template code paths. If a new template needs a feature the generic layout doesn't have (say, a video hero), we add it to the generic layout as an optional field on ProductData, then set it in the config. We never if (slug === "ai-voice-notes") anywhere.

  3. Every template shares the same checkout. The moment two templates need different checkout logic, we've failed. So far we've held the line: free apps, paid apps, single/multiple/enterprise tiers, bundles, partner referrals all flow through the same route.ts. You can see the result in the catalog: Fitness App and AI Voice Notes, along with Chat with PDF and AI Calorie Tracker, are structurally identical products from a code perspective. The only thing that differs is what's in the config.

What we still can't automate

Honest section, because playbooks lie by omission:

  • Screenshots. Someone has to actually run the template in a simulator, capture 16 screens, and drop them in Supabase Storage. The config references them by URL. That's the one manual step we haven't killed.

  • Copy quality. The config file has the schema, but the words still have to be good. We haven't found a way to make "write compelling hero copy" a config field.

  • Stripe price setup. Creating the three Stripe price IDs (single/multiple/enterprise) and pasting them into product_licenses rows is a two-minute manual step per template. Not worth automating yet. Everything else is a config change.

Where the config-first bet pays off

The compounding is showing up. When we added bundle support (see supabase/migrations/20260616120000_add_bundle_support.sql and the pricing changes in app/api/checkout/route.ts), it worked across every template on day one, because there's no per-template checkout code that could be out of sync. When we redesigned the FAQ accordion, every template got the new one for free.

The playbook is boring on purpose. Boring is the point. Read the Applighter docs for the customer-facing side.

FAQ

Q: Is this the same playbook you'd recommend for a solo indie dev shipping their first template? A: No. If you're shipping one product, hand-code the landing page. You'll iterate faster on copy. The config-first bet only pays off when you have three or more templates and can feel the copy-paste tax.

Q: Why config files instead of a CMS? A: Configs are versioned in git next to the app code they describe. That means the marketing page and the template can never drift. A CMS lets a marketer edit copy without shipping code, which is great for a single product and lethal when you have twelve.

Q: How long does a full template launch take end-to-end? A: About 90 minutes of engineering time (config file, DB rows, Stripe prices) plus whatever it takes to capture screenshots and write the launch blog. The bottleneck is content, not code. See Expo's build docs for the actual template build side.

Q: What about styling? Does every template look identical? A: The layout is identical; the colors and imagery are not. Every config specifies a brand color that ProductThemeProvider applies to buttons, badges, and accents. Fitness App is red-orange; AI Voice Notes is deep indigo; Chat with PDF is teal. Same skeleton, different skin.

Q: What happens when Applighter needs a template that genuinely doesn't fit the generic layout? A: We haven't hit that yet, and we've stress-tested it against everything from a weather app to a taxi booking app to an AI voice notes app to a PDF chat app. When we hit it, we'll extend ProductData. We won't add a per-template code path. That's rule #2.