{"id":68419,"plugin_id":"plugin_asdk_app_6ae0501df1c48191b1f2d6abe2a519a2","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-28T13:15:19.565Z","digest":"a83e9566d21b1046e6f9c2683bb2325d15f69ed3b62e14ae201852a62eda96aa","against":null,"payload":{"description":"Build and publish a full-stack web app or game with omg, with a database, sign-in and live updates between users. Use when the person asks for an app, a game, a tool, a vote, a board or anything they want to share by link.","included_files":[],"name":"build-omg-app","skill_md_contents":"---\nname: build-omg-app\ndescription: Build and publish a full-stack web app or game with omg, with a database, sign-in and live updates between users. Use when the person asks for an app, a game, a tool, a vote, a board or anything they want to share by link.\n---\n\n<!-- Generated by apps/mcp/scripts/build-plugin.ts from GUIDE in apps/mcp/src/apps.ts. Do not edit. -->\n\nThis server builds and publishes web apps for the person you are talking to. A published app gets its own address at https://<slug>.omgs.app with a database, sign-in, live updates between users, AI and media generation, and no setup.\n\nHow to build one:\n\n1. Write the app's files. Every deploy starts from the omg starter: Vite, React 19, TypeScript, Tailwind v4, shadcn/ui components in src/components/ui/, TanStack file routes in src/routes/, and the @omg-dev/* packages already installed. src/App.tsx is the home screen. Send only the files you write or change from the starter.\n2. Call omg_deploy_app with those files. It returns a slug, the app address, and a management link for the person.\n3. Call omg_app_status with the slug until the status is ready or failed. If it failed, read buildError and buildLogTail, fix the files, and deploy again.\n4. When it is ready, give the person the app address and the management link.\n\nData and multiplayer: define collections in schema.ts at the project root with defineSchema, collection and fields from @omg-dev/schema. Read and write them in React with useQuery from @omg-dev/sdk: const { data, create, update, remove } = useQuery<T>({ api: \"/api/<collection>\", collection: \"<collection>\" }). Every connected browser sees each change at once.\n\nA collection is private by default: each signed-in user sees only their own rows, and a visitor who is not signed in gets an error. Choose one for each collection:\n- Data that every visitor shares, such as votes, a chat, a leaderboard or a game state between players: add .scoped(\"global\"), for example collection({ fields: { ... } }).scoped(\"global\"). Visitors do not need to sign in.\n- Data that belongs to one person: keep the default, and wrap the app in <VibesAuthProvider><VibesAuthGuard>...</VibesAuthGuard></VibesAuthProvider> from @omg-dev/sdk so visitors sign in first. useUser() returns the signed-in user.\n- Data that everyone reads but only its author may change or delete, such as a guestbook, comments or posts: use .scoped(\"global\"), and add functions/<collection>.ts to guard the writes on the server. Hiding a button is not protection, because anyone can call the API. Wrap the app in <VibesAuthProvider> without the guard, so visitors can read. When a signed-out visitor writes, the server answers 401 and the provider opens sign-in by itself. The guestbook example below shows the full pattern.\n\nCustom server routes go in functions/<name>.ts and use named exports. A default export creates no route.\n- export async function POST(req: Request) { ... } is served at /api/<name>. GET, PUT, PATCH and DELETE work the same way, and receive the raw Request.\n- When <name> is a collection, export async function create(body), update(id, body) or remove(id) replaces that collection's automatic POST, PATCH or DELETE route.\n- Import { ctx, db, VibesHttpError } from \"@omg-dev/server\". ctx.userId is the signed-in user, or null. db.get(collection, id), db.insert(collection, data) and db.delete(collection, id) read and write rows, and every open browser still gets the change live. throw new VibesHttpError(403, \"message\") refuses a request.\n\nRules:\n- Every deploy replaces the app's source with the starter plus the files you send. When you change an existing app, send all of its files again, and pass the projectId from the first deploy.\n- Do not send package.json, node_modules, dist, .vibes, .git or .env files. Use only packages the starter already has.\n- Use the semantic color classes (bg-background, bg-primary, text-muted-foreground). The default Tailwind palette is removed, so a class like bg-indigo-600 has no effect.\n- Set the page title, which link previews show. Send index.html with your app's name in <title>:\n  <!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\" /><meta name=\"color-scheme\" content=\"light dark\" /><link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover\" /><title>App name</title></head><body><div id=\"root\"></div><script type=\"module\" src=\"/src/main.tsx\"></script></body></html>\n- Keep the first version small, usually schema.ts and src/App.tsx. The person waits while you write. Improve it after they see it working.\n- A new app opens only for people signed in to omg. Call omg_app_visibility with public when the person wants to share it with anyone who has the link.\n\nExample: a vote board that everyone with the link shares, with live counts. Two files.\n\nschema.ts\n```ts\nimport { defineSchema, collection, fields } from \"@omg-dev/schema\"\n\nexport default defineSchema({\n  collections: {\n    options: collection({ fields: { label: fields.string() } }).scoped(\"global\"),\n    // One row per vote. Counting rows never loses a vote when two people vote at once.\n    votes: collection({ fields: { optionId: fields.string(), voter: fields.string() } }).scoped(\"global\"),\n  },\n})\n```\n\nsrc/App.tsx\n```tsx\nimport { useState } from \"react\"\nimport { useQuery } from \"@omg-dev/sdk\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\n\ntype Option = { id: string; label: string }\ntype Vote = { id: string; optionId: string; voter: string }\n\nfunction voterId() {\n  let id = localStorage.getItem(\"voter\")\n  if (!id) { id = crypto.randomUUID(); localStorage.setItem(\"voter\", id) }\n  return id\n}\n\nexport default function App() {\n  const options = useQuery<Option>({ api: \"/api/options\", collection: \"options\" })\n  const votes = useQuery<Vote>({ api: \"/api/votes\", collection: \"votes\" })\n  const [label, setLabel] = useState(\"\")\n  const me = voterId()\n\n  return (\n    <main className=\"mx-auto max-w-md space-y-4 p-6\">\n      <form className=\"flex gap-2\" onSubmit={(e) => { e.preventDefault(); if (label.trim()) options.create({ label: label.trim() }); setLabel(\"\") }}>\n        <Input value={label} onChange={(e) => setLabel(e.target.value)} placeholder=\"Add an option\" />\n        <Button type=\"submit\">Add</Button>\n      </form>\n      {options.data.map((o) => {\n        const count = votes.data.filter((v) => v.optionId === o.id).length\n        const mine = votes.data.find((v) => v.optionId === o.id && v.voter === me)\n        return (\n          <div key={o.id} className=\"flex items-center justify-between rounded-lg border border-border p-3\">\n            <span>{o.label}</span>\n            <Button variant={mine ? \"secondary\" : \"default\"} onClick={() => (mine ? votes.remove(mine.id) : votes.create({ optionId: o.id, voter: me }))}>\n              {count}\n            </Button>\n          </div>\n        )\n      })}\n    </main>\n  )\n}\n```\n\nExample: a guestbook. Everyone reads every message, only signed-in people post, and only the author can delete a message. Three files.\n\nschema.ts\n```ts\nimport { defineSchema, collection, fields } from \"@omg-dev/schema\"\n\nexport default defineSchema({\n  collections: {\n    // Everyone reads every message. functions/messages.ts guards writes.\n    messages: collection({ fields: { text: fields.string(), author: fields.string(), authorName: fields.string() } }).scoped(\"global\"),\n  },\n})\n```\n\nfunctions/messages.ts\n```ts\nimport { ctx, db, VibesHttpError } from \"@omg-dev/server\"\n\n// These replace the automatic POST, PATCH and DELETE routes for messages.\nexport async function create(body: { text?: string }) {\n  if (!ctx.userId) throw new VibesHttpError(401, \"Sign in to post\")\n  const text = String(body?.text ?? \"\").trim().slice(0, 280)\n  if (!text) throw new VibesHttpError(400, \"Write a message\")\n  return db.insert(\"messages\", { text, author: ctx.userId, authorName: ctx.userName ?? ctx.userEmail ?? \"\" })\n}\n\nexport async function update() {\n  throw new VibesHttpError(403, \"Messages cannot be edited\")\n}\n\nexport async function remove(id: string) {\n  const row = await db.get(\"messages\", id)\n  if (!row) throw new VibesHttpError(404, \"Not found\")\n  if (row.author !== ctx.userId) throw new VibesHttpError(403, \"Only the author can delete this\")\n  await db.delete(\"messages\", id)\n  return { ok: true }\n}\n```\n\nsrc/App.tsx\n```tsx\nimport { useState } from \"react\"\nimport { useQuery, useUser, VibesAuthProvider } from \"@omg-dev/sdk\"\nimport { Button } from \"@/components/ui/button\"\n\ntype Message = { id: string; text: string; author: string; authorName: string; created_at: string }\n\nfunction Guestbook() {\n  const user = useUser()\n  const messages = useQuery<Message>({ api: \"/api/messages\", collection: \"messages\" })\n  const [text, setText] = useState(\"\")\n\n  return (\n    <main className=\"mx-auto max-w-md space-y-4 p-6\">\n      <h1 className=\"text-2xl font-semibold\">Guestbook</h1>\n      <form className=\"flex gap-2\" onSubmit={async (e) => { e.preventDefault(); if (!text.trim()) return; await messages.create({ text }); setText(\"\") }}>\n        <input className=\"flex-1 rounded-md border border-border bg-background px-3 py-2\" value={text} onChange={(e) => setText(e.target.value)} placeholder={user ? \"Leave a message\" : \"Sign in to leave a message\"} />\n        <Button type=\"submit\">Post</Button>\n      </form>\n      {messages.data.map((m) => (\n        <div key={m.id} className=\"flex items-start justify-between gap-2 rounded-lg border border-border p-3\">\n          <div><p>{m.text}</p><p className=\"text-sm text-muted-foreground\">{m.authorName}</p></div>\n          {user?.id === m.author && <Button variant=\"ghost\" onClick={() => messages.remove(m.id)}>Delete</Button>}\n        </div>\n      ))}\n    </main>\n  )\n}\n\nexport default function App() {\n  return <VibesAuthProvider><Guestbook /></VibesAuthProvider>\n}\n```\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}