DEV Community

Cover image for Why "Just Use Claude Code" Isn't a Mobile Strategy
Nikolas M I
Nikolas M I

Posted on • Originally published at the-path.hashnode.dev

Why "Just Use Claude Code" Isn't a Mobile Strategy

TL;DR

  • Claude Code is a tool, not a mobile app strategy — the strategy is the substrate it runs on.
  • The parts that break aren't code generation: EAS Build credentials, Supabase RLS contracts, Stripe webhook correctness, and AI keys leaking through EXPO_PUBLIC_*.
  • An agent can implement any of these correctly — it just won't know it should until the pattern already exists in the repo.
  • Give Claude Code a well-shaped repo (CLAUDE.md, visible patterns, typed boundaries) and it's 10x faster. Give it a blank folder and it's a liability with autocomplete.

Every React Native thread on Reddit right now has some version of the same take: "why would I buy a template when Claude Code exists?"

Fair question. Wrong framing.

Claude Code is a tool. A mobile app strategy is a substrate. Confusing the two is why so many AI-generated Expo apps die in the TestFlight queue.

Let me be specific about the failure modes.

Where Claude Code alone breaks on mobile

EAS Build is stateful

eas build --platform ios --profile production
Enter fullscreen mode Exit fullscreen mode

That command needs:

  • Apple ID + team ID
  • Distribution certificate
  • Provisioning profile matching the bundle ID
  • App Store Connect API key
  • A working eas.json with the right production profile
  • Metro bundler config that plays nice with your assets

None of this is code. It's account state and CLI ritual. Claude Code can generate a plausible-looking eas.json, but it can't create your certificates, can't upload them to Apple, and can't recover when your provisioning profile expires mid-build.

Supabase RLS is a contract, not a vibe

Ask an agent to "add auth" and you'll often see:

create policy "allow all" on public.users
  for all using (true);
Enter fullscreen mode Exit fullscreen mode

That's not a policy. That's a data breach with a create policy statement in front of it.

Real RLS looks like:

create policy "users read own row"
  on public.users for select
  using (auth.uid() = id);

create policy "users update own row"
  on public.users for update
  using (auth.uid() = id)
  with check (auth.uid() = id);
Enter fullscreen mode Exit fullscreen mode

And that's just the trivial case. Multi-tenant apps, invitation flows, sharing, admin roles — all require policy design before the agent starts writing. The agent can implement your contract. It shouldn't invent it. Docs: Supabase RLS reference.

Payments do not tolerate hallucination

A Stripe webhook handler generated by an agent:

export async function POST(req: Request) {
  const body = await req.json();
  if (body.type === 'checkout.session.completed') {
    await grantLicense(body.data.object.customer_email);
  }
  return new Response('ok');
}
Enter fullscreen mode Exit fullscreen mode

Everything is wrong. req.json() breaks signature verification (you need the raw body). There's no idempotency check. Email is a bad key for entitlement. There's no error handling. And where's the signing secret validated?

Correct version — abbreviated:

import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

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

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      raw,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!,
    );
  } catch {
    return new Response('bad signature', { status: 400 });
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object as Stripe.Checkout.Session;
    await upsertLicense({
      stripeCustomerId: session.customer as string,
      productId: session.metadata!.product_id,
      idempotencyKey: event.id,
    });
  }

  return new Response('ok');
}
Enter fullscreen mode Exit fullscreen mode

The point is not that Claude Code couldn't write the correct version. It's that it doesn't know it should until you tell it. That's the strategy — deciding what "correct" looks like before the prompt.

AI provider keys leak by default

If your Expo app imports:

import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.EXPO_PUBLIC_OPENAI_KEY,
});
Enter fullscreen mode Exit fullscreen mode

You just shipped your OpenAI key to every user of your app. EXPO_PUBLIC_* env vars are bundled into the JS. Anyone can curl your bundle and read it.

The strategy answer: every LLM call goes through a Supabase edge function that holds the key.

// Client
const res = await fetch(`${supabase.functions.url}/summarize`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${session.access_token}` },
  body: JSON.stringify({ text }),
});
Enter fullscreen mode Exit fullscreen mode
// Edge function
Deno.serve(async (req) => {
  const { text } = await req.json();
  const client = new OpenAI({ apiKey: Deno.env.get('OPENAI_KEY')! });
  const stream = await client.chat.completions.create({
    model: 'gpt-4o-mini',
    stream: true,
    messages: [{ role: 'user', content: text }],
  });
  return new Response(stream.toReadableStream());
});
Enter fullscreen mode Exit fullscreen mode

Claude Code can write both halves. It won't know to write the second half unless the pattern is already in the repo.

Where Claude Code is genuinely great

Given a well-shaped repo, Claude Code is transformative. Some concrete wins:

  • Screen scaffolding. Point it at app/(tabs)/index.tsx and ask for a variant. It preserves imports, styling, and data-fetching patterns.
  • Migration writing. Give it two existing Supabase migrations. Ask for a third. It matches your style, including RLS.
  • Edge function templating. Given one AI provider, it can generate an equivalent for another.
  • Codebase Q&A. "Where does the license grant happen?" — with file:line accuracy.

The requirement: a repo where those patterns are visible. That's what a template gives you.

Comparison

Concern Just Claude Code Claude Code + Substrate
Time to first TestFlight Days–weeks Same day
RLS + auth Invented per session Pre-designed, tested
EAS Build config You figure it out eas.json profiles included
Payments Hallucinated handlers Verified webhook + entitlements RLS
Design Mean-of-training-data Brand-tokenized
Agent conventions Reinvented every prompt CLAUDE.md, slash commands
Long-term maintenance Code drift Documented architecture

What "having a strategy" actually means

Decisions made before you prompt:

  1. Distribution — Expo + EAS, which profiles, which channels
  2. Backend — Supabase tables, RLS design, edge functions
  3. AI provider — model, streaming, cost cap, key location
  4. Payments — Stripe web / IAP / hybrid
  5. Design system — typography, tokens, dark mode, motion
  6. Agent conventions — CLAUDE.md, slash commands
  7. Store readiness — privacy manifest, subscription copy

You can build all of this yourself. It's 3–6 weeks of mid-level mobile engineering work per app. That's the honest baseline.

Or you start from a substrate that has it decided already — like the Applighter React Native + Expo templates, which ship with all of the above and a CLAUDE.md that turns Claude Code into a targeted execution engine rather than a random screen generator.

The one-line version

Claude Code is the leverage. A mobile app strategy is the fulcrum. You need both.

If you're building an AI feature, start with a substrate that ships the AI patterns already — AI Voice Notes and Chat with PDF are the two we recommend as starting points.

What are you building right now that hit one of these walls? Drop it in the comments — especially if it was the RLS one.

Top comments (0)