Eas hosting skill

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.

by expo·MIT license·★ 2,657 Stars on the repo·GitHub ↗

Use now

Files of Eas hosting

expo/main1 file shown
SKILL.md
Show the full text432 lines

EAS Hosting

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.

EAS 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.

When to Use API Routes

Use API routes when you need:

  • Server-side secrets — API keys, database credentials, or tokens that must never reach the client
  • Database operations — Direct database queries that shouldn't be exposed
  • Third-party API proxies — Hide API keys when calling external services (OpenAI, Stripe, etc.)
  • Server-side validation — Validate data before database writes
  • Webhook endpoints — Receive callbacks from services like Stripe or GitHub
  • Rate limiting — Control access at the server level
  • Heavy computation — Offload processing that would be slow on mobile

When NOT to Use API Routes

Avoid API routes when:

  • Data is already public — Use direct fetch to public APIs instead
  • No secrets required — Static data or client-safe operations
  • Real-time updates needed — Use WebSockets or services like Supabase Realtime
  • Simple CRUD — Consider Firebase, Supabase, or Convex for managed backends
  • File uploads — Use direct-to-storage uploads (S3 presigned URLs, Cloudflare R2)
  • Authentication only — Use Clerk, Auth0, or Firebase Auth instead

File Structure

API routes live in the app directory with +api.ts suffix:

app/
  api/
    hello+api.ts          → GET /api/hello
    users+api.ts          → /api/users
    users/[id]+api.ts     → /api/users/:id
  (tabs)/
    index.tsx

Basic API Route

// app/api/hello+api.ts
export function GET(request: Request) {
  return Response.json({ message: "Hello from Expo!" });
}

HTTP Methods

Export named functions for each HTTP method:

// app/api/items+api.ts
export function GET(request: Request) {
  return Response.json({ items: [] });
}

export async function POST(request: Request) {
  const body = await request.json();
  return Response.json({ created: body }, { status: 201 });
}

export async function PUT(request: Request) {
  const body = await request.json();
  return Response.json({ updated: body });
}

export async function DELETE(request: Request) {
  return new Response(null, { status: 204 });
}

Dynamic Routes

// app/api/users/[id]+api.ts
export function GET(request: Request, { id }: { id: string }) {
  return Response.json({ userId: id });
}

Request Handling

Query Parameters
export function GET(request: Request) {
  const url = new URL(request.url);
  const page = url.searchParams.get("page") ?? "1";
  const limit = url.searchParams.get("limit") ?? "10";

  return Response.json({ page, limit });
}
Headers
export function GET(request: Request) {
  const auth = request.headers.get("Authorization");

  if (!auth) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  return Response.json({ authenticated: true });
}
JSON Body
export async function POST(request: Request) {
  const { email, password } = await request.json();

  if (!email || !password) {
    return Response.json({ error: "Missing fields" }, { status: 400 });
  }

  return Response.json({ success: true });
}

Environment Variables

Use process.env for server-side secrets:

// app/api/ai+api.ts
export async function POST(request: Request) {
  const { prompt } = await request.json();

  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    },
    body: JSON.stringify({
      model: "gpt-4",
      messages: [{ role: "user", content: prompt }],
    }),
  });

  const data = await response.json();
  return Response.json(data);
}

Set environment variables:

  • Local: Create .env file (never commit)
  • EAS Hosting: Use eas env:create or Expo dashboard

CORS Headers

Add CORS for web clients:

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
};

export function OPTIONS() {
  return new Response(null, { headers: corsHeaders });
}

export function GET() {
  return Response.json({ data: "value" }, { headers: corsHeaders });
}

Error Handling

export async function POST(request: Request) {
  try {
    const body = await request.json();
    // Process...
    return Response.json({ success: true });
  } catch (error) {
    console.error("API error:", error);
    return Response.json({ error: "Internal server error" }, { status: 500 });
  }
}

Testing Locally

Start the development server with API routes:

npx expo serve

This starts a local server at http://localhost:8081 with full API route support.

Test with curl:

curl http://localhost:8081/api/hello
curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{"name":"Test"}'

Deployment to EAS Hosting

Prerequisites
npm install -g eas-cli
eas login
Deploy

Deploying 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.

# Export the web bundle (includes any API routes)
npx expo export -p web

# Deploy a preview (PR-style URL)
npx eas-cli@latest deploy

# Deploy to production
npx eas-cli@latest deploy --prod

Everything lands on EAS Hosting (Cloudflare Workers).

Environment Variables for Production
# Create a secret
eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production

# Or use the Expo dashboard
Custom Domain

Configure in eas.json or Expo dashboard.

Automate with EAS Workflows

Deploy the website (and API routes) on every push to main with a type: deploy workflow:

.eas/workflows/deploy.yml

name: Deploy

on:
  push:
    branches:
      - main

# https://docs.expo.dev/eas/workflows/syntax/#deploy
jobs:
  deploy_web:
    type: deploy
    params:
      prod: true

Preview deploys for pull requests use the same job type with prod: false:

name: Web PR Preview

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  preview:
    type: deploy
    params:
      prod: false

To author or validate workflow YAML beyond these examples, use the eas-workflows skill.

EAS Hosting Runtime (Cloudflare Workers)

API routes run on Cloudflare Workers. Key limitations:

Missing/Limited APIs
  • No Node.js filesystem — fs module unavailable
  • No native Node modules — Use Web APIs or polyfills
  • Limited execution time — 30 second timeout for CPU-intensive tasks
  • No persistent connections — WebSockets require Durable Objects
  • fetch is available — Use standard fetch for HTTP requests
Use Web APIs Instead
// Use Web Crypto instead of Node crypto
const hash = await crypto.subtle.digest(
  "SHA-256",
  new TextEncoder().encode("data")
);

// Use fetch instead of node-fetch
const response = await fetch("https://api.example.com");

// Use Response/Request (already available)
return new Response(JSON.stringify(data), {
  headers: { "Content-Type": "application/json" },
});
Database Options

Since filesystem is unavailable, use cloud databases:

  • Cloudflare D1 — SQLite at the edge
  • Turso — Distributed SQLite
  • PlanetScale — Serverless MySQL
  • Supabase — Postgres with REST API
  • Neon — Serverless Postgres

Example with Turso:

// app/api/users+api.ts
import { createClient } from "@libsql/client/web";

const db = createClient({
  url: process.env.TURSO_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

export async function GET() {
  const result = await db.execute("SELECT * FROM users");
  return Response.json(result.rows);
}

Calling API Routes from Client

// From React Native components
const response = await fetch("/api/hello");
const data = await response.json();

// With body
const response = await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "John" }),
});

Common Patterns

Authentication Middleware
// utils/auth.ts
export async function requireAuth(request: Request) {
  const token = request.headers.get("Authorization")?.replace("Bearer ", "");

  if (!token) {
    throw new Response(JSON.stringify({ error: "Unauthorized" }), {
      status: 401,
      headers: { "Content-Type": "application/json" },
    });
  }

  // Verify token...
  return { userId: "123" };
}

// app/api/protected+api.ts
import { requireAuth } from "../../utils/auth";

export async function GET(request: Request) {
  const { userId } = await requireAuth(request);
  return Response.json({ userId });
}
Proxy External API
// app/api/weather+api.ts
export async function GET(request: Request) {
  const url = new URL(request.url);
  const city = url.searchParams.get("city");

  const response = await fetch(
    `https://api.weather.com/v1/current?city=${city}&key=${process.env.WEATHER_API_KEY}`
  );

  return Response.json(await response.json());
}

Rules

  • NEVER expose API keys or secrets in client code
  • ALWAYS validate and sanitize user input
  • Use proper HTTP status codes (200, 201, 400, 401, 404, 500)
  • Handle errors gracefully with try/catch
  • Keep API routes focused — one responsibility per endpoint
  • Use TypeScript for type safety
  • Log errors server-side for debugging

Submitting Feedback

If you encounter errors, misleading or outdated information in this skill, report it so Expo can improve:

npx --yes submit-expo-feedback@latest --category skills --subject "eas-hosting" "<actionable feedback>"

Only submit when you have something specific and actionable to report. Include as much relevant context as possible. If 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.

1---
2name: eas-hosting
3description: 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.
4version: 1.0.0
5license: MIT
6---
7 
8# EAS Hosting
9 
10> **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.
11 
12EAS 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.
13 
14## When to Use API Routes
15 
16Use API routes when you need:
17 
18- **Server-side secrets** — API keys, database credentials, or tokens that must never reach the client
19- **Database operations** — Direct database queries that shouldn't be exposed
20- **Third-party API proxies** — Hide API keys when calling external services (OpenAI, Stripe, etc.)
21- **Server-side validation** — Validate data before database writes
22- **Webhook endpoints** — Receive callbacks from services like Stripe or GitHub
23- **Rate limiting** — Control access at the server level
24- **Heavy computation** — Offload processing that would be slow on mobile
25 
26## When NOT to Use API Routes
27 
28Avoid API routes when:
29 
30- **Data is already public** — Use direct fetch to public APIs instead
31- **No secrets required** — Static data or client-safe operations
32- **Real-time updates needed** — Use WebSockets or services like Supabase Realtime
33- **Simple CRUD** — Consider Firebase, Supabase, or Convex for managed backends
34- **File uploads** — Use direct-to-storage uploads (S3 presigned URLs, Cloudflare R2)
35- **Authentication only** — Use Clerk, Auth0, or Firebase Auth instead
36 
37## File Structure
38 
39API routes live in the `app` directory with `+api.ts` suffix:
40 
41```
42app/
43 api/
44 hello+api.ts → GET /api/hello
45 users+api.ts → /api/users
46 users/[id]+api.ts → /api/users/:id
47 (tabs)/
48 index.tsx
49```
50 
51## Basic API Route
52 
53```ts
54// app/api/hello+api.ts
55export function GET(request: Request) {
56 return Response.json({ message: "Hello from Expo!" });
57}
58```
59 
60## HTTP Methods
61 
62Export named functions for each HTTP method:
63 
64```ts
65// app/api/items+api.ts
66export function GET(request: Request) {
67 return Response.json({ items: [] });
68}
69 
70export async function POST(request: Request) {
71 const body = await request.json();
72 return Response.json({ created: body }, { status: 201 });
73}
74 
75export async function PUT(request: Request) {
76 const body = await request.json();
77 return Response.json({ updated: body });
78}
79 
80export async function DELETE(request: Request) {
81 return new Response(null, { status: 204 });
82}
83```
84 
85## Dynamic Routes
86 
87```ts
88// app/api/users/[id]+api.ts
89export function GET(request: Request, { id }: { id: string }) {
90 return Response.json({ userId: id });
91}
92```
93 
94## Request Handling
95 
96### Query Parameters
97 
98```ts
99export function GET(request: Request) {
100 const url = new URL(request.url);
101 const page = url.searchParams.get("page") ?? "1";
102 const limit = url.searchParams.get("limit") ?? "10";
103 
104 return Response.json({ page, limit });
105}
106```
107 
108### Headers
109 
110```ts
111export function GET(request: Request) {
112 const auth = request.headers.get("Authorization");
113 
114 if (!auth) {
115 return Response.json({ error: "Unauthorized" }, { status: 401 });
116 }
117 
118 return Response.json({ authenticated: true });
119}
120```
121 
122### JSON Body
123 
124```ts
125export async function POST(request: Request) {
126 const { email, password } = await request.json();
127 
128 if (!email || !password) {
129 return Response.json({ error: "Missing fields" }, { status: 400 });
130 }
131 
132 return Response.json({ success: true });
133}
134```
135 
136## Environment Variables
137 
138Use `process.env` for server-side secrets:
139 
140```ts
141// app/api/ai+api.ts
142export async function POST(request: Request) {
143 const { prompt } = await request.json();
144 
145 const response = await fetch("https://api.openai.com/v1/chat/completions", {
146 method: "POST",
147 headers: {
148 "Content-Type": "application/json",
149 Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
150 },
151 body: JSON.stringify({
152 model: "gpt-4",
153 messages: [{ role: "user", content: prompt }],
154 }),
155 });
156 
157 const data = await response.json();
158 return Response.json(data);
159}
160```
161 
162Set environment variables:
163 
164- **Local**: Create `.env` file (never commit)
165- **EAS Hosting**: Use `eas env:create` or Expo dashboard
166 
167## CORS Headers
168 
169Add CORS for web clients:
170 
171```ts
172const corsHeaders = {
173 "Access-Control-Allow-Origin": "*",
174 "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
175 "Access-Control-Allow-Headers": "Content-Type, Authorization",
176};
177 
178export function OPTIONS() {
179 return new Response(null, { headers: corsHeaders });
180}
181 
182export function GET() {
183 return Response.json({ data: "value" }, { headers: corsHeaders });
184}
185```
186 
187## Error Handling
188 
189```ts
190export async function POST(request: Request) {
191 try {
192 const body = await request.json();
193 // Process...
194 return Response.json({ success: true });
195 } catch (error) {
196 console.error("API error:", error);
197 return Response.json({ error: "Internal server error" }, { status: 500 });
198 }
199}
200```
201 
202## Testing Locally
203 
204Start the development server with API routes:
205 
206```bash
207npx expo serve
208```
209 
210This starts a local server at `http://localhost:8081` with full API route support.
211 
212Test with curl:
213 
214```bash
215curl http://localhost:8081/api/hello
216curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{"name":"Test"}'
217```
218 
219## Deployment to EAS Hosting
220 
221### Prerequisites
222 
223```bash
224npm install -g eas-cli
225eas login
226```
227 
228### Deploy
229 
230Deploying 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.
231 
232```bash
233# Export the web bundle (includes any API routes)
234npx expo export -p web
235 
236# Deploy a preview (PR-style URL)
237npx eas-cli@latest deploy
238 
239# Deploy to production
240npx eas-cli@latest deploy --prod
241```
242 
243Everything lands on EAS Hosting (Cloudflare Workers).
244 
245### Environment Variables for Production
246 
247```bash
248# Create a secret
249eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production
250 
251# Or use the Expo dashboard
252```
253 
254### Custom Domain
255 
256Configure in `eas.json` or Expo dashboard.
257 
258### Automate with EAS Workflows
259 
260Deploy the website (and API routes) on every push to main with a `type: deploy` workflow:
261 
262`.eas/workflows/deploy.yml`
263 
264```yaml
265name: Deploy
266 
267on:
268 push:
269 branches:
270 - main
271 
272# https://docs.expo.dev/eas/workflows/syntax/#deploy
273jobs:
274 deploy_web:
275 type: deploy
276 params:
277 prod: true
278```
279 
280Preview deploys for pull requests use the same job type with `prod: false`:
281 
282```yaml
283name: Web PR Preview
284 
285on:
286 pull_request:
287 types: [opened, synchronize]
288 
289jobs:
290 preview:
291 type: deploy
292 params:
293 prod: false
294```
295 
296To author or validate workflow YAML beyond these examples, use the `eas-workflows` skill.
297 
298## EAS Hosting Runtime (Cloudflare Workers)
299 
300API routes run on Cloudflare Workers. Key limitations:
301 
302### Missing/Limited APIs
303 
304- **No Node.js filesystem** — `fs` module unavailable
305- **No native Node modules** — Use Web APIs or polyfills
306- **Limited execution time** — 30 second timeout for CPU-intensive tasks
307- **No persistent connections** — WebSockets require Durable Objects
308- **fetch is available** — Use standard fetch for HTTP requests
309 
310### Use Web APIs Instead
311 
312```ts
313// Use Web Crypto instead of Node crypto
314const hash = await crypto.subtle.digest(
315 "SHA-256",
316 new TextEncoder().encode("data")
317);
318 
319// Use fetch instead of node-fetch
320const response = await fetch("https://api.example.com");
321 
322// Use Response/Request (already available)
323return new Response(JSON.stringify(data), {
324 headers: { "Content-Type": "application/json" },
325});
326```
327 
328### Database Options
329 
330Since filesystem is unavailable, use cloud databases:
331 
332- **Cloudflare D1** — SQLite at the edge
333- **Turso** — Distributed SQLite
334- **PlanetScale** — Serverless MySQL
335- **Supabase** — Postgres with REST API
336- **Neon** — Serverless Postgres
337 
338Example with Turso:
339 
340```ts
341// app/api/users+api.ts
342import { createClient } from "@libsql/client/web";
343 
344const db = createClient({
345 url: process.env.TURSO_URL!,
346 authToken: process.env.TURSO_AUTH_TOKEN!,
347});
348 
349export async function GET() {
350 const result = await db.execute("SELECT * FROM users");
351 return Response.json(result.rows);
352}
353```
354 
355## Calling API Routes from Client
356 
357```ts
358// From React Native components
359const response = await fetch("/api/hello");
360const data = await response.json();
361 
362// With body
363const response = await fetch("/api/users", {
364 method: "POST",
365 headers: { "Content-Type": "application/json" },
366 body: JSON.stringify({ name: "John" }),
367});
368```
369 
370## Common Patterns
371 
372### Authentication Middleware
373 
374```ts
375// utils/auth.ts
376export async function requireAuth(request: Request) {
377 const token = request.headers.get("Authorization")?.replace("Bearer ", "");
378 
379 if (!token) {
380 throw new Response(JSON.stringify({ error: "Unauthorized" }), {
381 status: 401,
382 headers: { "Content-Type": "application/json" },
383 });
384 }
385 
386 // Verify token...
387 return { userId: "123" };
388}
389 
390// app/api/protected+api.ts
391import { requireAuth } from "../../utils/auth";
392 
393export async function GET(request: Request) {
394 const { userId } = await requireAuth(request);
395 return Response.json({ userId });
396}
397```
398 
399### Proxy External API
400 
401```ts
402// app/api/weather+api.ts
403export async function GET(request: Request) {
404 const url = new URL(request.url);
405 const city = url.searchParams.get("city");
406 
407 const response = await fetch(
408 `https://api.weather.com/v1/current?city=${city}&key=${process.env.WEATHER_API_KEY}`
409 );
410 
411 return Response.json(await response.json());
412}
413```
414 
415## Rules
416 
417- NEVER expose API keys or secrets in client code
418- ALWAYS validate and sanitize user input
419- Use proper HTTP status codes (200, 201, 400, 401, 404, 500)
420- Handle errors gracefully with try/catch
421- Keep API routes focused — one responsibility per endpoint
422- Use TypeScript for type safety
423- Log errors server-side for debugging
424 
425## Submitting Feedback
426If you encounter errors, misleading or outdated information in this skill, report it so Expo can improve:
427```bash
428npx --yes submit-expo-feedback@latest --category skills --subject "eas-hosting" "<actionable feedback>"
429```
430Only submit when you have something specific and actionable to report. Include as much relevant context as possible.
431If 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.
432 

Discussion

Alternatives

API and interface designGuides stable API and interface design. Use when designing APIs, module boundaries, or any public interface. Use when creating REST or GraphQL endpoints, defining type contracts between modules, or establishing boundaries between frontend and backend.Coding · MITContext7Pulls up-to-date, version-specific library docs and code examples into the prompt so the AI stops inventing old APIs.Coding · MITContext7 Documentation LookupFetch up-to-date documentation and code examples for any library, framework, SDK, CLI tool, or cloud service. Use whenever the user asks about a specific library — even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot — because training data may not reflect recent API changes or version updates. Always use for: API syntax questions, configuration options, version migration issues, "how do I" questions mentioning a library name, debugging that involves library-specific behavior, setup instructions, and CLI tool usage. Use even when you think you know the answer. Do not rely on training data for API details, signatures, or configuration options — they are frequently out of date. Prefer this over web search for library documentation.Coding · MITAdaptyv Bio Foundry APIHow to use the Adaptyv Bio Foundry API and Python SDK for protein experiment design, submission, and results retrieval. Use this skill whenever the user mentions Adaptyv, Foundry API, protein binding assays, protein screening experiments, BLI/SPR assays, thermostability assays, or wants to submit protein sequences for experimental characterization. Also trigger when code imports `adaptyv`, `adaptyv_sdk`, or `FoundryClient`, or references `foundry-api-public.adaptyvbio.com`.Science · MIT