Deployment
This guide covers deploying Sema Web applications to production. The deployment approach depends on whether your app uses LLM features.
If you want the broader recommended app structure first, start with Building a Sema Web App. This page is focused on the deployment step.
Static Hosting (No LLM)
If your app does not use any llm/* functions, deployment is straightforward: serve static files. Sema Web runs entirely in the browser via WebAssembly.
For development and quick embeds, you can still load .sema source directly. For production, prefer a compiled .vfs archive built with the CLI.
Development
<script type="text/sema" src="/app.sema"></script>
<script type="module">
import { SemaWeb } from "@sema-lang/sema-web";
await SemaWeb.init();
</script>Production
Build the app once:
sema build --target web app.sema -o public/app.vfsThen deploy the compiled archive:
my-app/
public/
index.html
app.vfs<!DOCTYPE html>
<html>
<head>
<title>My Sema App</title>
</head>
<body>
<div id="app"></div>
<script type="text/sema" src="/app.vfs"></script>
<script type="module">
import { SemaWeb } from "@sema-lang/sema-web";
await SemaWeb.init();
</script>
</body>
</html>The HTML shape stays the same. Only the src changes from source (.sema) to a compiled archive (.vfs).
With LLM Proxy
When your app uses llm/chat, llm/complete, or other LLM functions, you also need a server-side proxy to hold API keys. The proxy is a single serverless function.
Architecture
Static host Serverless function LLM Provider
┌───────────────┐ ┌─────────────────┐ ┌────────────┐
│ HTML + .sema │──POST──│ /api/llm/* │──API──>│ OpenAI │
│ sema-web WASM │ │ (holds API key) │ │ Anthropic │
└───────────────┘ └─────────────────┘ │ Gemini ... │
└────────────┘Vercel Deployment
This is the recommended approach. Vercel serves both static files and serverless functions from a single project.
1. Project structure:
my-sema-app/
public/
index.html
app.vfs
api/
llm/
[...path].ts # Proxy catch-all route
package.json
vercel.json2. Install dependencies:
npm install @sema-lang/sema-web @sema-lang/llm-proxy3. Create the proxy function:
// api/llm/[...path].ts
import { createVercelHandler } from "@sema-lang/llm-proxy/vercel";
export const { GET, POST, OPTIONS } = createVercelHandler({
provider: "openai",
apiKey: process.env.OPENAI_API_KEY!,
defaultModel: "gpt-4o",
rateLimit: { maxRequests: 30 },
});A plain export default createVercelHandler({...}) won't work here — Vercel's api/ directory expects either named exports per HTTP method (what the destructuring above produces) or a fetch-shaped default export, not an object of {GET, POST, OPTIONS} methods as the default export itself.
4. Build the app for production:
sema build --target web app.sema -o public/app.vfs5. Create the HTML entry point:
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>My AI App</title>
<script type="text/sema" src="/app.vfs"></script>
<script type="module">
import { SemaWeb } from "@sema-lang/sema-web";
await SemaWeb.create({
llmProxy: "/api/llm",
});
</script>
</head>
<body>
<div id="app"></div>
</body>
</html>6. Set environment variables:
vercel env add OPENAI_API_KEY
# Paste your API key when prompted7. Deploy:
vercel deploy # Preview deployment
vercel --prod # Production deploymentNetlify Deployment
1. Project structure:
my-sema-app/
public/
index.html
app.vfs
netlify/
edge-functions/
llm.ts
netlify.toml
package.json2. Create the edge function:
// netlify/edge-functions/llm.ts
import { createNetlifyHandler } from "@sema-lang/llm-proxy/netlify";
export default createNetlifyHandler({
provider: "anthropic",
apiKey: Netlify.env.get("ANTHROPIC_API_KEY")!,
defaultModel: "claude-sonnet-4-20250514",
});
export const config = { path: "/api/llm/*" };Use the Netlify.env global to read environment variables in Edge Functions, not Deno.env directly — Netlify.env is scoped to variables declared for the Functions scope and is what Netlify's current docs document for this runtime.
3. Build the app for production:
sema build --target web app.sema -o public/app.vfs4. Configure Netlify:
# netlify.toml
[build]
publish = "public"
[[edge_functions]]
path = "/api/llm/*"
function = "llm"5. Set environment variables in the Netlify dashboard under Site settings > Environment variables.
6. Deploy:
netlify deploy --prodEnvironment Variables
API keys must be set as server-side environment variables. Never include them in client-side code or commit them to source control.
| Variable | Provider | Required for |
|---|---|---|
OPENAI_API_KEY | OpenAI | provider: "openai" |
ANTHROPIC_API_KEY | Anthropic | provider: "anthropic" |
GOOGLE_API_KEY | Gemini | provider: "gemini" |
GROQ_API_KEY | Groq | provider: "groq" |
MISTRAL_API_KEY | Mistral | provider: "mistral" |
XAI_API_KEY | xAI | provider: "xai" |
Ollama runs locally and does not require an API key.
CORS Configuration
By default, the proxy allows all origins (cors: "*"). For production, restrict it to your domain:
createVercelHandler({
provider: "openai",
apiKey: process.env.OPENAI_API_KEY!,
cors: "https://my-app.example.com",
});If your static files and proxy are served from the same domain (as in the Vercel example above), CORS is not an issue -- same-origin requests do not need CORS headers.
Production Checklist
- [ ] API keys stored in environment variables, not in code
- [ ] Proxy authentication enabled (shared token or custom
verify) - [ ] CORS restricted to your domain
- [ ] Rate limiting configured to prevent abuse
- [ ]
maxBodySizeset appropriately (default: 1MB) - [ ] Static assets served with appropriate cache headers