Platform integration
Rork and Wire AI: self-serve setup
Every other page here starts from an API key you already have. On Rork you do not have one, and there is nobody to ask for it: the app was built by chatting, so the usual path where someone provisions a tenant and pastes a key back to you does not exist. That is what the public signup route on the Wire server removes. One unauthenticated POST returns an app id, a minted key, and the server URL to point at, and Rork's environment variable panel is already the right place to put all three. The second decision is the device key, and on a generated app the right answer is to let the kit mint it.
Install
$npxexpo install @react-native-async-storage/async-storage$npminstall @wireai/activation wireai-rnAPI surface on this page read against @wireai/activation 0.13.5 (npm latest, read 2026-08-04) and the Wire AI signup route (probed live 2026-07-28, 201). These are the versions the calls below were checked against, not a claim that this exact file is running in production somewhere.
Mint the tenant before you install anything
The signup route is public and takes no bearer token, because the whole point is that a generated app can provision itself. app_name is the only required field, trimmed before it is bounded, so an empty or whitespace name and anything over 60 characters come back 422 rather than creating a junk tenant. email and source are optional and are only stashed on the app for a later claim flow.
The 201 carries everything the kit needs and one thing it does not need yet. claim_url is where the claim flow will live once it ships, the place a signup tenant gets attached to a console login. It is not wired up today: the server returns that URL as a constant, there is no claim endpoint behind it, and the console login has no registration path to land on. The app runs fully without it, because api_key is everything the SDK needs. That key is the tenant's real production key and it is returned exactly once, at creation, so treat this response as the thing you copy out rather than a call you can repeat.
Copy app_id out of the response too, rather than assuming it. The server slugifies the name you sent, and if that slug is already taken it appends -2, then -3, so "My Rork App" can come back as my-rork-app-2. Everything downstream, the environment variable and the appId you pass the kit, has to be the app_id the 201 actually returned.
1curl -X POST https://wire-rn-dynamic-onboarding.fly.dev/v1/signup \
2 -H "Content-Type: application/json" \
3 -d '{"app_name":"My Rork App","email":"you@example.com","source":"rork"}'
4
5# 201 Created
6# {
7# "app_id": "my-rork-app",
8# "api_key": "wai_00000000000000000000000000000000",
9# "server_url": "https://wire-rn-dynamic-onboarding.fly.dev",
10# "claim_url": "https://getwireai.com/console"
11# }Put the three values in Rork's environment variables
Rork's docs describe an environment variable panel so a project can hold API values without standing a backend up behind it. The Expo half of the contract is the prefix: EXPO_PUBLIC_ is what gets inlined into the client bundle at build time, and those inlined values are what wireConfigFromEnv reads. Naming the three variables with that prefix in the panel is what pairs the two, and it assumes the panel exposes its variables to the Expo build, which their docs do not spell out.
wireConfigFromEnv reads EXPO_PUBLIC_WIREAI_API_KEY, EXPO_PUBLIC_WIREAI_SERVER_URL and EXPO_PUBLIC_WIREAI_APP_ID, and it returns null when the API key or the server URL is missing. Gate on that null instead of asserting: either of those two missing disables the kit and says so once in dev, it does not crash the app you just generated. The app id is the one that does not gate. Leave it out and nothing returns null, appId silently falls back to "default", and the kit mis-namespaces this tenant's storage keys under an id that is not yours. That is why the snippet below passes appId explicitly rather than trusting the panel, and why requiredVars still names all three: a preflight that asserts the three catches the panel entry you skipped, while the kit itself only refuses to start on two of them.
One constraint the panel cannot warn you about. If you alias process.env into a local variable, or a generated helper does, all three values come back undefined at runtime: Metro only inlines static process.env.EXPO_PUBLIC_* member access, matched at build time, and nothing in the build tells you it failed to match.
1import { wireConfigFromEnv, WIRE_ENV_VARS } from "@wireai/activation";
2
3// null when the API key or the server URL is missing.
4// Gate on it. Do not assert it: a missing var means Wire is off, not broken.
5// APP_ID does NOT gate: missing, it falls back to "default" and
6// mis-namespaces this tenant's storage keys, which is why appId is
7// passed explicitly here and why requiredVars asserts all three.
8export const wireConfig = wireConfigFromEnv({ appId: "my-rork-app" });
9
10// The three names, exported so a preflight can assert them itself.
11export const requiredVars = WIRE_ENV_VARS;Let the kit own the device key
user_context.device_key is the only field that joins an onboarding session to everything the app reports afterwards. A generated app usually has no device id management at all, and here that is the good case: with a storage adapter present and no device_key in userContext, WireOnboarding injects the same per-install key createAnalytics and createWireActivation mint and persist, and the activated funnel joins with no wiring from you. That is the correct path for a Rork app and it is a decision, not a default you inherited.
The decision that follows is the one worth writing down. The moment your app owns a device id of its own, it owns it on three surfaces: the analytics instance, the lifecycle hook, and WireOnboarding. Pass it to two of them and the third opens a separate id space, the join lands empty, and nothing throws. Since 0.13.0 a mount that injected its own key while the process demonstrably holds a host-supplied one warns and prints both ids, because adopting the other key would be the kit guessing which of two ids you meant from construction order.
Note the shape of the gate below. useLifecycleEvents takes its config as LifecycleConfig or undefined by design, so pass undefined rather than returning early. An early return above a hook is a rules-of-hooks bug that only shows up on the build where the environment variables are missing, which is the build you are least likely to test.
1import AsyncStorage from "@react-native-async-storage/async-storage";
2import { Stack } from "expo-router";
3import { useLifecycleEvents } from "@wireai/activation";
4
5import { wireConfig } from "@/lib/wire";
6
7export default function RootLayout() {
8 // The hook accepts undefined on purpose, so it stays unconditional.
9 // storage is what mints and PERSISTS the per-install device key.
10 useLifecycleEvents(
11 wireConfig
12 ? {
13 serverUrl: wireConfig.serverUrl,
14 apiKey: wireConfig.apiKey,
15 appId: wireConfig.appId,
16 storage: AsyncStorage,
17 }
18 : undefined,
19 );
20
21 return <Stack screenOptions={{ headerShown: false }} />;
22}Mount the flow, and pass storage there too
config is not nullable on WireOnboarding, so this is the one place an early return is right: render your own screen when Wire is off. Pass storage on this surface as well. It is doing two jobs at once, persisting the session seed so an app kill resumes instead of minting a second start, and satisfying the gate that lets the kit inject the join key.
fallbackFlow is not optional thinking on a generated app. The signup tenant runs a scripted flow rather than the model path, so the usual model timeout is not your failure mode here, but a cold network on first launch still is. Give it something static to render and the user finishes either way.
1import AsyncStorage from "@react-native-async-storage/async-storage";
2import { WireOnboarding } from "@wireai/activation";
3
4import { wireConfig } from "@/lib/wire";
5import { StaticOnboarding } from "@/components/StaticOnboarding";
6
7export function OnboardingScreen({ onDone }: { onDone: () => void }) {
8 // config is NOT nullable on this component, so gate the render here.
9 if (!wireConfig) return <StaticOnboarding onDone={onDone} />;
10
11 return (
12 <WireOnboarding
13 config={wireConfig}
14 // No device_key in userContext + storage present
15 // = the kit injects its own join key.
16 storage={AsyncStorage}
17 onComplete={onDone}
18 fallbackFlow={<StaticOnboarding onDone={onDone} />}
19 />
20 );
21}What breaks
The failure modes when Rork and Wire AI share an app. None of them throws, which is why they survive code review.
wireai-rn and the Metro wrapper are part of the install, not extras
The kit renders through wireai-rn, a required peer at 0.2.3 or newer, so it goes in the same install line as @wireai/activation. The other half is Metro. Wrapping your config with withWireOnboarding from @wireai/activation/metro is required, and it is what prevents a second copy of React ending up in the bundle, which crashes React Native at runtime. In metro.config.js that is one require of withWireOnboarding and one call around getDefaultConfig. Rork's GitHub sync is two way, so if that file is not reachable from the chat surface, export the project, add the wrapper, and sync back.
The key comes back once, in the 201, and nowhere else
api_key in the signup response is the tenant's production key at the moment it is created. This call deliberately creates no client and no password, so there is no second call that reads the key back and no account it belongs to. Copy it into the environment variable panel from that response, or call the route again and get a different tenant. If you would rather have the key stay readable, take an account at getwireai.com/signup instead when that door is open: it binds the key to your console.
A signup tenant is forced to the scripted flow, server-side
The route forces plan free and onboarding mode static and seeds a non-empty default script, ignoring anything a client sends, so a signup flood can never reach the model path. Practically: your first flow is the two-question default, not a generated one. That is the tier behaving as designed, not a broken install, and moving a signup tenant off it is what the claim flow will cover once it ships.
Five signups per IP per hour by default, then 429 with Retry-After
Five per IP per hour is the default, not a constant: both that number and the global backstop across all IPs are environment overridable, so a self-hosted server can sit anywhere. The global window is checked first, which means a busy server can 429 you on your first call of the hour. Both run on a one hour sliding window, and the limiter runs after body validation, so a malformed name is a clean 422 rather than eating a slot. Scripting the signup call into a template that runs on every build will exhaust the window and hand your users a 429.
Adding your own device id makes it your job on all three surfaces
Auto injection covers a host that owns no device id. It does not cover a host that owns one and forgot it on one mount. Analytics, useLifecycleEvents and WireOnboarding all have to carry the same key, or you get a third id space and an activated funnel that reads zero while every call returns success.
No storage, or storage that rejects, means no join key at all
The kit will not inject a key it cannot persist, because a key that changes every launch is worse than none: the server counts min_sessions by distinct opens grouped on device_key, so a per-launch key pins that counter at 1 and inflates device counts at the same time. Since 0.13.0 the gate reads whether the write actually succeeded, so a locked or full AsyncStorage lands on the same declined path as no adapter, with a warning that names it.
Questions people actually ask
Does this work with Rork Max?
No. Rork Max builds native SwiftUI apps compiled with Xcode, and @wireai/activation is a React Native package. Rork Pro is the lane, because Pro generates React Native and Expo projects.
Do I need a backend, Supabase or Firebase for this?
No. Rork's environment variables exist so a project can hold API keys without connecting a backend, and the kit talks to the Wire server directly from the app. If you later add Rork Backend or Supabase for your own data, nothing here changes.
What is claim_url for, and do I have to use it?
You cannot use it yet. claim_url is where the claim flow will live once it ships, the point where the tenant signup minted gets attached to a console login so you can see the funnel and move off the scripted default. Today it is not wired: the server returns that URL as a constant, no claim endpoint sits behind it, and the console login has no registration path. The app runs fully without it, because the api_key from the 201 is everything the SDK needs.
Can I keep the code once it works?
Yes. Rork's GitHub integration is two way, so you can export the generated project, edit these files in an editor, and sync back. The wiring on this page is plain TypeScript in your project, not something the platform owns.
Sources
- Rork FAQ (Rork Pro uses React Native and Expo; Pro vs Max) (read 2026-07-28)
- Rork docs: technical FAQ (React Native and Expo, cross platform) (read 2026-07-28)
- Rork docs: connecting an API (environment variables for API keys) (read 2026-07-28)
- Rork docs: code export FAQ (two way GitHub sync on paid plans) (read 2026-07-28)
Wire it into your app
Analytics is free to 299k events a month, forever, on any app. 15 founder seats get the whole engine free, 4 filled.