{"id":15529,"plugin_id":"plugin_asdk_app_6a90296230f4819180d857f69ebe7f2e","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:11:33.904Z","digest":"01030690b02a8735588a2f38c38a9edd4132a0ee74ad5cd1e6655f2bc7f67c0d","against":null,"payload":{"name":"eas-hosting","description":"EAS service (paid). Deploy Expo websites and Expo Router API routes to EAS Hosting - export the web bundle, run eas deploy for production and PR preview URLs, manage environment secrets and custom domains, and work within the Cloudflare Workers runtime. Also covers authoring API routes (+api.ts handlers, HTTP methods, request handling, CORS). Use when deploying an Expo web app or API routes, setting up EAS Hosting, or configuring hosting environments and domains. Not for native builds or store releases - use the eas-app-stores skill for those.","included_files":[{"relative_path":"agents/openai.yaml","size_in_bytes":462}],"skill_md_contents":"---\nname: eas-hosting\ndescription: EAS service (paid). Deploy Expo websites and Expo Router API routes to EAS Hosting - export the web bundle, run eas deploy for production and PR preview URLs, manage environment secrets and custom domains, and work within the Cloudflare Workers runtime. Also covers authoring API routes (+api.ts handlers, HTTP methods, request handling, CORS). Use when deploying an Expo web app or API routes, setting up EAS Hosting, or configuring hosting environments and domains. Not for native builds or store releases - use the eas-app-stores skill for those.\nversion: 1.0.0\nlicense: MIT\n---\n\n# EAS Hosting\n\n> **EAS service - costs apply.** EAS Hosting is a paid Expo Application Services product with free-tier limits; production deploys use your plan's request and bandwidth allowance. See https://expo.dev/pricing. Authoring API routes and exporting the web bundle are free and open source, and you can self-host the exported server output instead of EAS Hosting.\n\nEAS Hosting deploys your Expo **web app and API routes** to Expo's managed edge (Cloudflare Workers). Export the web bundle with `npx expo export -p web` and ship it with `eas deploy` - the same command deploys any Expo Router API routes bundled alongside it. This skill covers deploying a website, authoring API routes, and the hosting runtime; see the Deployment section below for the deploy workflow.\n\n## When to Use API Routes\n\nUse API routes when you need:\n\n- **Server-side secrets** — API keys, database credentials, or tokens that must never reach the client\n- **Database operations** — Direct database queries that shouldn't be exposed\n- **Third-party API proxies** — Hide API keys when calling external services (OpenAI, Stripe, etc.)\n- **Server-side validation** — Validate data before database writes\n- **Webhook endpoints** — Receive callbacks from services like Stripe or GitHub\n- **Rate limiting** — Control access at the server level\n- **Heavy computation** — Offload processing that would be slow on mobile\n\n## When NOT to Use API Routes\n\nAvoid API routes when:\n\n- **Data is already public** — Use direct fetch to public APIs instead\n- **No secrets required** — Static data or client-safe operations\n- **Real-time updates needed** — Use WebSockets or services like Supabase Realtime\n- **Simple CRUD** — Consider Firebase, Supabase, or Convex for managed backends\n- **File uploads** — Use direct-to-storage uploads (S3 presigned URLs, Cloudflare R2)\n- **Authentication only** — Use Clerk, Auth0, or Firebase Auth instead\n\n## File Structure\n\nAPI routes live in the `app` directory with `+api.ts` suffix:\n\n```\napp/\n  api/\n    hello+api.ts          → GET /api/hello\n    users+api.ts          → /api/users\n    users/[id]+api.ts     → /api/users/:id\n  (tabs)/\n    index.tsx\n```\n\n## Basic API Route\n\n```ts\n// app/api/hello+api.ts\nexport function GET(request: Request) {\n  return Response.json({ message: \"Hello from Expo!\" });\n}\n```\n\n## HTTP Methods\n\nExport named functions for each HTTP method:\n\n```ts\n// app/api/items+api.ts\nexport function GET(request: Request) {\n  return Response.json({ items: [] });\n}\n\nexport async function POST(request: Request) {\n  const body = await request.json();\n  return Response.json({ created: body }, { status: 201 });\n}\n\nexport async function PUT(request: Request) {\n  const body = await request.json();\n  return Response.json({ updated: body });\n}\n\nexport async function DELETE(request: Request) {\n  return new Response(null, { status: 204 });\n}\n```\n\n## Dynamic Routes\n\n```ts\n// app/api/users/[id]+api.ts\nexport function GET(request: Request, { id }: { id: string }) {\n  return Response.json({ userId: id });\n}\n```\n\n## Request Handling\n\n### Query Parameters\n\n```ts\nexport function GET(request: Request) {\n  const url = new URL(request.url);\n  const page = url.searchParams.get(\"page\") ?? \"1\";\n  const limit = url.searchParams.get(\"limit\") ?? \"10\";\n\n  return Response.json({ page, limit });\n}\n```\n\n### Headers\n\n```ts\nexport function GET(request: Request) {\n  const auth = request.headers.get(\"Authorization\");\n\n  if (!auth) {\n    return Response.json({ error: \"Unauthorized\" }, { status: 401 });\n  }\n\n  return Response.json({ authenticated: true });\n}\n```\n\n### JSON Body\n\n```ts\nexport async function POST(request: Request) {\n  const { email, password } = await request.json();\n\n  if (!email || !password) {\n    return Response.json({ error: \"Missing fields\" }, { status: 400 });\n  }\n\n  return Response.json({ success: true });\n}\n```\n\n## Environment Variables\n\nUse `process.env` for server-side secrets:\n\n```ts\n// app/api/ai+api.ts\nexport async function POST(request: Request) {\n  const { prompt } = await request.json();\n\n  const response = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,\n    },\n    body: JSON.stringify({\n      model: \"gpt-4\",\n      messages: [{ role: \"user\", content: prompt }],\n    }),\n  });\n\n  const data = await response.json();\n  return Response.json(data);\n}\n```\n\nSet environment variables:\n\n- **Local**: Create `.env` file (never commit)\n- **EAS Hosting**: Use `eas env:create` or Expo dashboard\n\n## CORS Headers\n\nAdd CORS for web clients:\n\n```ts\nconst corsHeaders = {\n  \"Access-Control-Allow-Origin\": \"*\",\n  \"Access-Control-Allow-Methods\": \"GET, POST, PUT, DELETE, OPTIONS\",\n  \"Access-Control-Allow-Headers\": \"Content-Type, Authorization\",\n};\n\nexport function OPTIONS() {\n  return new Response(null, { headers: corsHeaders });\n}\n\nexport function GET() {\n  return Response.json({ data: \"value\" }, { headers: corsHeaders });\n}\n```\n\n## Error Handling\n\n```ts\nexport async function POST(request: Request) {\n  try {\n    const body = await request.json();\n    // Process...\n    return Response.json({ success: true });\n  } catch (error) {\n    console.error(\"API error:\", error);\n    return Response.json({ error: \"Internal server error\" }, { status: 500 });\n  }\n}\n```\n\n## Testing Locally\n\nStart the development server with API routes:\n\n```bash\nnpx expo serve\n```\n\nThis starts a local server at `http://localhost:8081` with full API route support.\n\nTest with curl:\n\n```bash\ncurl http://localhost:8081/api/hello\ncurl -X POST http://localhost:8081/api/users -H \"Content-Type: application/json\" -d '{\"name\":\"Test\"}'\n```\n\n## Deployment to EAS Hosting\n\n### Prerequisites\n\n```bash\nnpm install -g eas-cli\neas login\n```\n\n### Deploy\n\nDeploying ships your web bundle and any Expo Router API routes together - `eas deploy` handles both. The export runs whether you have a full website, an API-routes-only backend, or both.\n\n```bash\n# Export the web bundle (includes any API routes)\nnpx expo export -p web\n\n# Deploy a preview (PR-style URL)\nnpx eas-cli@latest deploy\n\n# Deploy to production\nnpx eas-cli@latest deploy --prod\n```\n\nEverything lands on EAS Hosting (Cloudflare Workers).\n\n### Environment Variables for Production\n\n```bash\n# Create a secret\neas env:create --name OPENAI_API_KEY --value sk-xxx --environment production\n\n# Or use the Expo dashboard\n```\n\n### Custom Domain\n\nConfigure in `eas.json` or Expo dashboard.\n\n### Automate with EAS Workflows\n\nDeploy the website (and API routes) on every push to main with a `type: deploy` workflow:\n\n`.eas/workflows/deploy.yml`\n\n```yaml\nname: Deploy\n\non:\n  push:\n    branches:\n      - main\n\n# https://docs.expo.dev/eas/workflows/syntax/#deploy\njobs:\n  deploy_web:\n    type: deploy\n    params:\n      prod: true\n```\n\nPreview deploys for pull requests use the same job type with `prod: false`:\n\n```yaml\nname: Web PR Preview\n\non:\n  pull_request:\n    types: [opened, synchronize]\n\njobs:\n  preview:\n    type: deploy\n    params:\n      prod: false\n```\n\nTo author or validate workflow YAML beyond these examples, use the `eas-workflows` skill.\n\n## EAS Hosting Runtime (Cloudflare Workers)\n\nAPI routes run on Cloudflare Workers. Key limitations:\n\n### Missing/Limited APIs\n\n- **No Node.js filesystem** — `fs` module unavailable\n- **No native Node modules** — Use Web APIs or polyfills\n- **Limited execution time** — 30 second timeout for CPU-intensive tasks\n- **No persistent connections** — WebSockets require Durable Objects\n- **fetch is available** — Use standard fetch for HTTP requests\n\n### Use Web APIs Instead\n\n```ts\n// Use Web Crypto instead of Node crypto\nconst hash = await crypto.subtle.digest(\n  \"SHA-256\",\n  new TextEncoder().encode(\"data\")\n);\n\n// Use fetch instead of node-fetch\nconst response = await fetch(\"https://api.example.com\");\n\n// Use Response/Request (already available)\nreturn new Response(JSON.stringify(data), {\n  headers: { \"Content-Type\": \"application/json\" },\n});\n```\n\n### Database Options\n\nSince filesystem is unavailable, use cloud databases:\n\n- **Cloudflare D1** — SQLite at the edge\n- **Turso** — Distributed SQLite\n- **PlanetScale** — Serverless MySQL\n- **Supabase** — Postgres with REST API\n- **Neon** — Serverless Postgres\n\nExample with Turso:\n\n```ts\n// app/api/users+api.ts\nimport { createClient } from \"@libsql/client/web\";\n\nconst db = createClient({\n  url: process.env.TURSO_URL!,\n  authToken: process.env.TURSO_AUTH_TOKEN!,\n});\n\nexport async function GET() {\n  const result = await db.execute(\"SELECT * FROM users\");\n  return Response.json(result.rows);\n}\n```\n\n## Calling API Routes from Client\n\n```ts\n// From React Native components\nconst response = await fetch(\"/api/hello\");\nconst data = await response.json();\n\n// With body\nconst response = await fetch(\"/api/users\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ name: \"John\" }),\n});\n```\n\n## Common Patterns\n\n### Authentication Middleware\n\n```ts\n// utils/auth.ts\nexport async function requireAuth(request: Request) {\n  const token = request.headers.get(\"Authorization\")?.replace(\"Bearer \", \"\");\n\n  if (!token) {\n    throw new Response(JSON.stringify({ error: \"Unauthorized\" }), {\n      status: 401,\n      headers: { \"Content-Type\": \"application/json\" },\n    });\n  }\n\n  // Verify token...\n  return { userId: \"123\" };\n}\n\n// app/api/protected+api.ts\nimport { requireAuth } from \"../../utils/auth\";\n\nexport async function GET(request: Request) {\n  const { userId } = await requireAuth(request);\n  return Response.json({ userId });\n}\n```\n\n### Proxy External API\n\n```ts\n// app/api/weather+api.ts\nexport async function GET(request: Request) {\n  const url = new URL(request.url);\n  const city = url.searchParams.get(\"city\");\n\n  const response = await fetch(\n    `https://api.weather.com/v1/current?city=${city}&key=${process.env.WEATHER_API_KEY}`\n  );\n\n  return Response.json(await response.json());\n}\n```\n\n## Rules\n\n- NEVER expose API keys or secrets in client code\n- ALWAYS validate and sanitize user input\n- Use proper HTTP status codes (200, 201, 400, 401, 404, 500)\n- Handle errors gracefully with try/catch\n- Keep API routes focused — one responsibility per endpoint\n- Use TypeScript for type safety\n- Log errors server-side for debugging\n\n## Submitting Feedback\nIf you encounter errors, misleading or outdated information in this skill, report it so Expo can improve:\n```bash\nnpx --yes submit-expo-feedback@latest --category skills --subject \"eas-hosting\" \"<actionable feedback>\"\n```\nOnly submit when you have something specific and actionable to report. Include as much relevant context as possible.\nIf an AI agent repeatedly failed or the user had to take over an Expo task, load the expo-skill-feedback skill and follow its eval-candidate flow instead of reusing the command above.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}