Build and deliver an omg.dev app
Turn the user's product description into a working, verified result. Keep setup work with the agent. Ask only about product choices that materially change the result.
Ask one question per question call, and put at most one decision in it. A second question in the same call is not shown to the user on every client. If the user has not said what the app should do, ask that in plain words and wait for the answer. Do not build a placeholder product in its place. If a question times out, ask again once in a normal message and end the turn.
Establish the project
- In Quick Chat, call
omg_create_projectonce with a short descriptive name. Settemplatetoexpofor an Expo or universal mobile app. The Expo template already contains a working todo flow with data stored on the phone, Expo Router, Liquid Glass, Lucide icons, web support, and EAS configuration. Use the returnedrepo.cwdfor every command, edit, check, and deployment. Inspectomg_list_reposbefore retrying an uncertain create call. - In a New Project session, the current directory is already the project. It contains Git, a README, and this skill. Do not create a second project.
- Preserve
.git. Do not overwrite an existing folder or move the current Quick Chat into the new project.
Choose the delivery path
-
For a website, web app, or API, use the supported omg.dev runtime unless the user requests another platform. In a blank project, scaffold the official omg.dev React template before writing product code. The current directory already contains Git and omg.dev instructions, so generate into a temporary directory inside it, copy the scaffold into the project, and remove only that temporary directory:
scaffold="$(mktemp -d .omg-web-scaffold-XXXXXX)" bunx --bun create-omg@latest "$scaffold" --template react-ts --no-install cp -a "$scaffold"/. . rm -rf "$scaffold" bun installThis template supplies
@omg-dev/vite-plugin,vibes-build, server functions, database support, and analytics. Do not substitutecreate-viteor a hand-written Vite scaffold. Readhttps://docs.omg.dev/llms.txtand only the relevant parts ofhttps://docs.omg.dev/llms-full.txtbefore choosing additional packages or API contracts. -
For a game (racing, arcade, puzzle, kids' learning, word, 3D), create the project with
template: "web-game"and read.agents/skills/omg-web-game-builder/SKILL.mdinstead of designing an engine. Its step 1 comes before any code edit:bun run devin the background on port 5174, thenomg_expose_portwith port 5174 and withoutexpoGo, then tell the user the preview is ready in the card below. The Expo preview steps in this skill (port 8081,expoGo: true) do not apply to a web game. Usetemplate: "expo-game"and.agents/skills/omg-game-builder/SKILL.mdonly when the user asks for a game to install on iPhone or Android, or names Expo or a native app. -
For an Expo app, start from the managed
expotemplate and follow "Expo app: the fast path" below. Adapt its screens and its data store instead of rebuilding configuration. Keep secrets in hosted server routes. A production native build needs any server deployed at a secure origin; do not treat a sandbox tunnel as production hosting. -
Use one live Metro server for Expo Web and Expo Go in a Cloud Computer. Expose its port through
omg_expose_port; do not assume a global Expo CLI, Xcode, a simulator, or machine-specific tools exist. -
When the app is compatible with Expo Go,
omg_expose_portputs theexps://Expo Go link on a preview card in the omg.dev app. Tell the user the preview is ready and to open it from the card below. Do not paste the rawexps://URL in your reply; the card already shows it. Paste it only where the user cannot see the card, such as a reply sent withomg_send_to_originto a messaging channel, or when the user asks for the link. Do not use Expo tunnel, LAN exposure,exp.direct, or ngrok. If Expo Go cannot load a required native module, use a development build when authorized or report that limit clearly. -
TestFlight, App Store submission, paid services, domains, and third-party production accounts are separate delivery actions. Do them only when the user requests them and the required account is available. TestFlight needs the user's Expo account and paid Apple Developer account, but an EAS cloud build does not need Xcode or a Mac in the sandbox. Use supported login flows and never ask for passwords or tokens in chat.
Expo app: the fast path
A new user is waiting. The first screen must reach their phone within about 3 minutes, before any feature work. Follow these steps in order:
omg_create_projectwithtemplate: "expo". Ifnode_modulesis missing, runbun installonce.- Preview before you write any code. Call
omg_expose_portwith port 8081 andexpoGo: true. Then runbash scripts/start-expo-preview.sh <expoGo.proxyUrl> 8081from the project directory, with a 240000 ms shell timeout. When it printsSandbox proxy answers: HTTP 200, present the web preview first: "Your app is ready. The card below shows it running, and it works on any device." Then mention the phone in one sentence: "To try it on your own phone, tap Your phone on the card." When theomg_expose_portresult hassimulatorAvailable: true, add one more sentence: "You can also try it on an iPhone right in the card: tap Simulator." Do not promise the simulator when that field is false or missing. Do not pasteexpoGo.url; the card shows it. Metro reloads the app on every save, so the preview follows your edits from here on. - Write the screens, in a few larger edits rather than one small edit per turn. Replace the template home screen in
src/app/index.tsx; do not add a second index route. - Run
bun run typecheckand fix what it reports. It also checks the routes and fails on a second home screen or a rootapp/folder. Then runbun run check:ui, open its PNGs, and fix what is wrong (see Build step 4). Do not write automated tests unless the user asks. - Deploy once with
omg_deploy(see below). The data stays on the phone, so this is a static web build. - Finish with
omg_ship.
Do not browse node_modules, fetch documentation, or read the template file by file before the first preview: the summary below is enough. Do not write your own browser test loops or screenshot scripts: bun run check:ui does that. If you cannot view images, read its text report only.
The template:
src/app/index.tsx: the home screen, and the only file for/. Replace its content with your home screen. Keep every screen insrc/app/: Expo Router ignores a rootapp/folder whilesrc/appexists. For tabs, move the home screen tosrc/app/(tabs)/index.tsxand deletesrc/app/index.tsx, because both answer/and the old one wins. The template file has a header, an input with an add button, a list with toggle and delete, an error card, and a reload button. Styles are in oneStyleSheetat the bottom; the theme constants are at the top (INK,CORAL,BACKGROUND,SURFACE,SURFACE_BORDER).src/app/_layout.tsx: an Expo Router stack with no header, insideKeyboardProvider. A new screen is a new file insrc/app/(stats.tsxis/stats). Navigate withrouter.push("/stats")or<Link href="/stats">fromexpo-router.src/lib/tasks.ts: the data, stored on the phone with AsyncStorage (localStorage on the web).tasksApi.list(),create(title),update(id, patch),remove(id). Keep this file. For each new kind of item, copy its shape with its own storage key.- Icons:
lucide-react-native, for example<Plus color={CORAL} size={22} />. Glass surfaces:GlassViewfromexpo-glass-effect. - Colours: set every theme constant from the design before you style screens. Cards, rows and sheets use the design's own surface colour. The template's translucent white
SURFACEsuits only its light starter; left on a dark or coloured design, every card comes out white. UseGlassViewonly where the design shows frosted glass. On Android it draws no glass, so give it a background colour. - Safe areas: wrap each screen in
SafeAreaViewfromreact-native-safe-area-contextwithedges={["top", "bottom"]}, or useuseSafeAreaInsets(). Never importSafeAreaViewfromreact-native: it is iOS-only, so on Android the header goes under the status bar and the tab bar under the gesture bar. - Keyboard: put screens with a text input in
KeyboardAwareScrollViewfromsrc/lib/keyboard(bottomOffset={24},keyboardShouldPersistTaps="handled"), as the template does. For a field pinned to the bottom, useKeyboardStickyViewfrom the same file. Import them fromsrc/lib/keyboard, never fromreact-native-keyboard-controllerdirectly: its.web.tsxtwin keeps the static web export working. Do not use React Native'sKeyboardAvoidingView: on iOS the screen moves after the keyboard, slowly.
Data: store it on the phone by default, with the tasks.ts pattern. Use hosted data only when the user asks for accounts or data shared between people. Then add sign-in with @omg-dev/sdk and .scoped("user") collections in schema.ts. Never call /api/... with a raw fetch: a hosted collection answers an anonymous phone with 401 Authentication required.
For hosted data, install the latest @omg-dev/sdk and deploy the backend before testing its shared data in Metro. Keep .omg/project.json in the project directory. The hosted web preview uses that registered app identity and signs its owner in through the SDK. Use <VibesAuthProvider> and SDK requests. Do not add a separate preview login, forward dashboard cookies, or replace the user's app permissions. A physical phone still needs the app's normal sign-in and the deployed backend origin.
Metro rules: use only ports 8081 to 8099, because Expo Go links work only in that range. Start Metro only with the script; npx and ss are not installed on every Computer.
Building blocks (Expo)
The template ships tested helpers in src/kit/. Import them; do not rewrite them. They work on iOS, Android and web. Each file starts with a comment that shows its use.
overlay.tsx:useConfirm()anduseSheet().<OverlayProvider>is already in_layout.tsx. Useif (await confirm({ title, destructive: true }))for every delete or confirm.Alert.alertwith buttons does nothing on web. UseuseSheet().open(node)for sheets and forms. Do not stackModalcomponents: the second one draws behind the first on web.ActionRow.tsx: a pressable row with a delete or other action beside it. A button nested inside a pressable row also fires the row on web.image.ts:await toPersistentImage(pickerAsset). Call the picker withbase64: true, quality: 0.5and store the returned string. A picker URI on web is ablob:URL that is gone after a reload.use-timer.ts:usePersistedTimer(key, durationMs)withstart,pause,resume,reset, andformatClock. It stores the end time, so a reload does not reset it. Use it for every timer or countdown.use-viewport.ts:useViewport()anduseIsWide(). In the static web exportuseWindowDimensions()can give width 0 on first render. PreferflexandmaxWidth; use the hook only when you need a number.
Shared data between people (a shared list, a trade, a group): use the hosted @omg-dev/sdk path in "Data" above. Do not write a local server. If a local server is unavoidable, answer OPTIONS and send these headers on every response: Access-Control-Allow-Origin with the request Origin (or *), Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS, and Access-Control-Allow-Headers: content-type,authorization. A missing PUT or DELETE fails only in the browser.
Build
- Convert the request into a small observable done condition. Make reasonable visual and technical choices when the user did not specify them.
- Scaffold inside the project directory. For a managed Expo template, install its pinned dependencies and preserve
.omg/template.json; do not replace it with another starter. For a blank project, install the current documented dependencies and configuration. Do not install a retired omg.dev CLI or ask the user for infrastructure tokens; runtime tools provide the Cloud credential. - Implement the real path. If the product needs shared data, authentication, uploads, or server work, use the documented hosted APIs and verify persistence and access control. Do not replace requested shared behavior with mock data or browser-only storage.
- Do not write automated tests unless the user asks for them. Verify with the self-check instead: run
bun run typecheck, thenbun run check:ui. The check builds the web export, starts the app'sserverorbackendscript if there is one, opens every route at 390x844, and saves one PNG per route in.omg/ui-check/. It reports blank pages, error boundaries, console errors, failed requests, a crashed server, and buttons or links that another element covers (for example an "Add" button under a tab bar). Then open each PNG with the image viewer and compare it with the request and any design references. Fix what is wrong and run the check again until the report is clean. State which surface you checked; a web run is not proof of native behavior. - Update the README with the product purpose, local run command, architecture, and delivery notes. Commit the finished source locally so later omg.dev sessions start from a complete baseline.
Sign-in for a hosted web app
omg.dev owns sign-in for apps at <slug>.omgs.app. Never build a sign-in form, a password field, or your own session store.
- Private (the default): every visitor signs in to omg.dev before the app opens. The app needs no sign-in UI.
- Public with sign-in: use this when the user wants a landing page for visitors. Set the app to
publicwithomg_app_visibility. Show the landing page whenuseAuth().userfrom@omg-dev/sdkis empty, with a plain link tohttps://app.omg.dev/access?return=<encoded current URL>. The link returns to the same page signed in. A person already signed in to omg.dev returns at once with no form. ThenuseAuth().userhasid,email, andname, and.scoped("user")collections hold only that person's data. After one Sign in, later visits sign the person in automatically before the page loads. Do not add redirects to the sign-in page or a "remember me" option. - Sign out only with
useAuth().signOut(). Any other way, such as clearing app state or reloading, signs the person back in on the next visit. - On a custom domain there is no automatic sign-in. Keep the Sign in link.
- Wrap the app in
<VibesAuthProvider>. Do not put<VibesAuthGuard>around the landing page, and do not run user-scoped queries on it. - Verify the signed-out landing page and the Sign in link on the deployed URL. You cannot sign in as the user. Say that the signed-in path was not checked by you.
Deploy and prove it
- For a new website, web app, or API, a working hosted preview is the default result unless the user asks for local-only work or publication needs new authority.
- If the script exits non-zero, read the log tail it prints, fix the cause, and run it again. Do not retry with other flags such as
--host 0.0.0.0. - The preview card has levels. "Web" (Expo Web from the same Metro server) is the default and shows the app inline on any device, with no account. "Simulator" (only when
simulatorAvailableis true) streams the app running in Expo Go on an iPhone simulator into the card; it needs no Expo account and no install. "Your phone" opens the app in Expo Go. Present the web preview first, then mention the phone. Give the userexpoGo.url, which starts withexps://, only where they cannot see the card. - Expo Go on a physical iPhone opens a dev server only when Expo CLI on the Computer is signed in to an Expo account and Expo Go is signed in to the same account. Otherwise it shows "You need to be signed in to Expo Go and Expo CLI". The proxy, a tunnel, and
--offlinedo not change this; Android, the iOS Simulator, and Expo Web are not affected. The script printsWARNING: Expo CLI is not signed inor the account name. After the warning, still present the web preview first. When the user wants the app on their iPhone, offer it in these words: "Create a free Expo account". Your final message must also carry that offer while Expo CLI is not signed in: end it with "To open it on your iPhone, Create a free Expo account: tap "Your phone" on the preview card, then "Create free account" (or "I have one")." Never end with "try it on your phone via Expo Go" or a QR code alone, because Expo Go on an iPhone cannot open the app until the user has an account. Tell them to tap "Your phone" on the preview card, then "Create free account", or "I have one" if they already have an Expo account. "Create free account" opens the Computer view with expo.dev/signup (Google, Apple, or email). The user creates the account there, and the card then continues into the Expo CLI sign-in by itself. "I have one" opens the expo.dev sign-in page. After that the card shows three steps: get Expo Go, sign in to Expo Go as the account it names, and open in Expo Go. Metro restarts by itself when it must. Expo's terms forbid automated sign-ups: never create, fill in, or submit an Expo account form for the user. Do not sign in for the user and never ask for a password or token in chat. If the user asks you to start it instead, run./node_modules/.bin/expo login --browserin the project: the page opens in the Computer browser, and the user signs in there. - A Computer that sleeps stops Metro. When the user asks to restart the preview, or the preview card says it stopped, call
omg_expose_portagain withexpoGo: trueand run the script with the new proxy URL. - Use only the URLs returned by
omg_expose_port. Do not also try LAN addresses, localhost URLs,exp.direct, ngrok, or Expo's tunnel. Do not start competing development servers. - If
omg_expose_portfails, confirm that the server is still running, is bound to0.0.0.0, and answers on the same port. Report the exact tool error if it still fails. Do not silently switch the web preview to another exposure method. - If
omg_expose_portis unavailable or the session is on a local computer, report that limit. Do not invent a public URL, expose credentials, or depend on this repository's simulator, SSH hosts, filesystem layout, or globally installed tools. - The Expo Go capability link is short-lived. If it expires, restart the preview the same way.
- For a website, use
omg_deploywhen the user wants a durable hosted deployment. It stays private to omg.dev users. Make it public only when the user wants that or asks for a landing page for visitors (see "Sign-in for a hosted web app"). Publishing the app to TestFlight or the App Store is a separate step the user asks for. - Call
omg_deploywithcwdset to the project directory andwait: true. It waits at most 45 seconds. If the result haspending: true, callomg_deploy_statuswith the returned slug until the build is ready or failed. Do not start a second deploy. Reuse.omg/project.jsonon later deploys. After success, commit this non-secret file with the source so the app keeps one identity. - Open the returned URL. Exercise the main user path and any relevant backend operation against the deployed app. A successful build or upload is not proof that the deployment works.
- On a phone, the live URL shows the omg install bar below the app. The app becomes a card above the bar, so the bar covers nothing. Do not move or hide it. Local previews and
bun run check:uido not show it. - After you publish an app's web version, save its store screenshots. Open the live URL at phone size (390x844). Capture its 3 main screens with real-looking sample data, and no empty states, error screens or dev overlays. Put the images in the app's public folder under
omg-store/(served as/omg-store/1.png; png, jpg or webp, flat names). That folder is public even when the app needs sign-in. Publish again. Callomg_app_identitywithscreenshotUrlsset to their live https links, in order. Its store page (omg.dev/apps/) shows them. Capture and publish them again when a later change visibly changes those screens. - Show the live result with
omg_display_imagewhen a screenshot is useful. Return the actual clickable URL and state which parts were verified. - If build, deployment, login, or verification fails, report the exact remaining state. Never describe a local build, artifact, or pending native submission as deployed.
Finish the assigned task with omg_ship only after the requested result is verified.