Frontend Integration Cookbook
Connect a frontend to a Spala backend with base URL, routes, auth headers, file upload, realtime, and env vars.
Frontend Integration Cookbook
Use this page when a frontend developer or AI coding agent needs to connect an app UI to a Spala project API.
Specific project handoff required
Public docs cannot contain a private project's real base URL, route list, auth payloads, upload modes, or realtime channels. To integrate a specific project, start with that project's generated API Docs export from Settings → API Docs. Use this page to understand the stable Spala integration patterns around that project contract.
If the frontend developer or AI coding agent does not have dashboard access, first send the packet from External Frontend Handoff. It defines the exact API_BASE_URL, OpenAPI/SDK files, auth routes, upload mode, realtime path, and CORS origin needed for a safe integration.
Guaranteed discovery flow
Spala projects are configurable, so the public docs cannot safely derive a real project API base URL or route list from a slug. The guaranteed integration flow is:
1. Open the project in https://dashboard.spala.ai. 2. Copy the API base URL from Lite Mode or Project Overview. 3. Open Settings → API Docs. 4. Export or copy the project contract: Markdown docs, JSON docs, OpenAPI JSON, SDK files, or snippets where enabled. 5. Wire the frontend from that generated contract. 6. Use this cookbook for the standard request, auth, upload, realtime, environment, and CORS patterns.
Example OpenAPI shape:
https://docs.spala.ai/examples/spala-openapi-example.json
That file is an example contract shape only. Real projects expose their own generated API contract from Settings → API Docs.
REST docs are not the whole integration
OpenAPI JSON and generated SDK files describe REST endpoints. They may not cover realtime channel protocol, signed-upload storage CORS, required direct-upload PUT headers, cookie/CSRF behavior, refresh/logout rules, OAuth callback URLs, or mobile-specific storage and resume behavior. Include those details separately in the frontend handoff when the project uses them.
Project integration artifacts
Use the generated project artifacts as the source of truth. In the hosted dashboard, Settings → API Docs gives the same contract in UI form. For authenticated project tooling, the generated docs routes are:
| Artifact | Route | Access |
| --- | --- | --- |
| Markdown API docs | GET {{PROJECT_BASE_URL}}/api/__internal/docs | Builder authentication |
| JSON API docs | GET {{PROJECT_BASE_URL}}/api/__internal/docs?format=json | Builder authentication |
| OpenAPI JSON | GET {{PROJECT_BASE_URL}}/api/__internal/docs/openapi.json | Builder authentication |
| TypeScript SDK | GET {{PROJECT_BASE_URL}}/api/__internal/docs/sdk.ts | Builder authentication |
| Public Markdown docs | GET {{PROJECT_BASE_URL}}/api/docs | Only when public docs are enabled in Security |
| Public JSON docs | GET {{PROJECT_BASE_URL}}/api/docs?format=json | Only when public docs are enabled in Security |
| Swagger UI | Settings → API Docs | Use the dashboard UI unless your project explicitly exposes a public Swagger route |
For shared-runtime projects, the project base can include a scoped path such as https://shared.spala.ai/PROJECT_ID. Do not strip that path. Use the exact base URL and full URLs shown by the generated docs.
Do not expose builder docs by accident
The api/__internal/docs routes require builder authentication. Only enable public api/docs when you intentionally want the project API contract to be readable without builder auth.
Find the API base URL
The API base URL is shown in Lite Mode and Project Overview. Copy it from the project workspace after the project has been published or previewed.
Use the full base URL plus the endpoint path:
SPALA_API_BASE_URL=https://your-project-host.example
GET {{SPALA_API_BASE_URL}}/api/cases
POST {{SPALA_API_BASE_URL}}/api/cases
Do not guess the host. Use the value shown in the project.
Contract discovery matrix
| Need | Where to find it | Stable rule | | --- | --- | --- | | API base URL | Lite Mode or Project Overview | Copy the displayed value; do not infer it from project name | | REST routes | Endpoints or Settings → API Docs | Use the exact /api/... paths in the project | | Markdown/JSON docs | Settings → API Docs or GET /api/__internal/docs | Use the generated project artifact as the source of truth | | OpenAPI JSON | Settings → API Docs or GET /api/__internal/docs/openapi.json | Use the generated project artifact as the source of truth | | TypeScript SDK | Settings → API Docs or GET /api/__internal/docs/sdk.ts | Use the generated project artifact as the source of truth | | Auth routes | Endpoints or API Docs | Common routes are /api/auth/login and /api/auth/register, but project docs are authoritative | | Upload mode | API Docs and file upload endpoint | Multipart and signed URL flows are different contracts | | Realtime URL | Channels and generated project docs | Use the project WebSocket path, auth mode, message envelope, heartbeat, replay, and permissions | | Browser origins | Settings → Security → CORS Allowed Origins | Add the frontend origin and republish if browser calls are blocked |
Environment variables
For Vite:
VITE_SPALA_API_BASE_URL=https://your-project-host.example
export const SPALA_API_BASE_URL = import.meta.env.VITE_SPALA_API_BASE_URL;
For Next.js:
NEXT_PUBLIC_SPALA_API_BASE_URL=https://your-project-host.example
export const SPALA_API_BASE_URL = process.env.NEXT_PUBLIC_SPALA_API_BASE_URL!;
Public endpoint call
const response = await fetch(`${SPALA_API_BASE_URL}/api/cases`);
if (!response.ok) {
throw new Error(`Spala API error: ${response.status}`);
}
const cases = await response.json();
Protected endpoint call
Protected endpoints expect a user JWT from your login flow.
const response = await fetch(`${SPALA_API_BASE_URL}/api/me`, {
headers: {
Authorization: `Bearer ${userJwt}`,
},
});
if (response.status === 401) {
// Send the user back through your login flow.
}
const profile = await response.json();
Auth contract
Common generated auth endpoints return a user token that frontends send as a bearer token. Treat the project API Docs as the authoritative contract for exact route names and response shape.
Typical login response:
{
"token": "USER_JWT"
}
Some projects also return a user object or a data wrapper. The generated API Docs for the project are authoritative; the stable frontend rule is to extract the token value returned by the login endpoint and send it as a bearer token.
Typical protected request:
Authorization: Bearer USER_JWT
Recommended frontend behavior:
- Keep tokens out of logs and source control. - On 401, clear local session state and send the user through login again. - On 403, keep the user logged in but show a permissions message. - Confirm refresh-token behavior from the generated project API Docs if the project implements refresh tokens.
Session variants
| Variant | Frontend request behavior | Notes | | --- | --- | --- | | Bearer JWT | Send Authorization: Bearer USER_JWT on protected requests | Most common generated pattern | | Bearer JWT with refresh | Retry once through the project refresh route, then send the new token | Use only if generated API Docs expose refresh | | Cookie session | Send credentials: "include" and follow CSRF rules if configured | Requires exact CORS credentials settings | | API key or custom header | Send the header documented by the project | Common for server-to-server or internal tools |
Browser clients cannot invent session behavior. If the project docs do not mention refresh tokens, cookie sessions, or CSRF, assume bearer-token login and re-login on expiry.
Token storage
Store user tokens according to your frontend threat model. Prefer short-lived tokens, avoid logging tokens, and never commit real tokens into source code or docs.
Login example
The exact route names depend on your project. A common pattern is:
const response = await fetch(`${SPALA_API_BASE_URL}/api/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
});
if (!response.ok) {
throw new Error("Login failed");
}
const { token } = await response.json();
File upload example
Use FormData for multipart upload endpoints.
const form = new FormData();
form.append("file", file);
const response = await fetch(`${SPALA_API_BASE_URL}/api/upload`, {
method: "POST",
headers: {
Authorization: `Bearer ${userJwt}`,
},
body: form,
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
const uploadedFile = await response.json();
If your project uses S3 or signed URLs, follow the route contract generated in Settings → API Docs. Do not mix multipart upload and signed URL flows unless the project explicitly exposes both.
Upload contract matrix
| Upload mode | Request | Response | When to use | | --- | --- | --- | --- | | Multipart endpoint | POST /api/upload with FormData | JSON record for the stored file | Simple browser file uploads through the project API | | Signed URL | Request signed URL, then upload directly to storage | Storage URL or file metadata | Larger files or direct-to-S3 style flows | | Addon-backed storage | Project-specific endpoint calls an addon such as S3 | Project-defined JSON | When storage provider behavior is part of backend logic |
Signed URL frontend sequence:
const signResponse = await fetch(`${SPALA_API_BASE_URL}/api/uploads/sign`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userJwt}`,
},
body: JSON.stringify({
filename: file.name,
contentType: file.type,
size: file.size,
}),
});
const { uploadUrl, fileId, headers = {} } = await signResponse.json();
const uploadResponse = await fetch(uploadUrl, {
method: "PUT",
headers,
body: file,
});
if (!uploadResponse.ok) {
throw new Error(`Storage upload failed: ${uploadResponse.status}`);
}
await fetch(`${SPALA_API_BASE_URL}/api/uploads/${fileId}/complete`, {
method: "POST",
headers: {
Authorization: `Bearer ${userJwt}`,
},
});
For signed URLs, the storage bucket CORS policy may also need to allow the frontend origin. That is separate from the Spala project API CORS setting.
Signed upload contract fields to include:
| Field | Why it matters | | --- | --- | | Required PUT headers | Some storage signatures require exact Content-Type, checksum, or metadata headers | | Content type | The uploaded Content-Type may need to match the signing input exactly | | Size limits | The project or storage provider may reject files above the signed limit | | Retry/cancel behavior | Retrying may require a new signed URL if the old one expires | | Complete body | The finalize route may need storage key, file id, checksum, or public URL | | File object schema | Frontends need the returned id, url, name, size, and metadata shape |
CORS and browser request behavior
Server-to-server calls and curl do not prove that browser calls will work. Browser frontends also need a matching CORS configuration.
Checklist for browser calls:
1. Add the exact frontend origin in Settings → Security → CORS Allowed Origins. 2. Include the scheme and port, such as http://localhost:5173 or https://app.example.com. 3. Publish the backend API after changing CORS settings. 4. If the frontend sends bearer tokens, allow the Authorization request header. 5. If the frontend sends JSON bodies, allow Content-Type. 6. If the project uses cookie sessions, enable credentialed requests and call fetch with credentials: "include".
Use CORS and Security for the owner-side settings checklist.
Example preflight the browser may send:
OPTIONS /api/cases HTTP/1.1 Origin: https://app.example.com Access-Control-Request-Method: POST Access-Control-Request-Headers: authorization, content-type
Expected allowed response shape:
Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS Access-Control-Allow-Headers: Authorization, Content-Type
If credentials are enabled, Access-Control-Allow-Origin must be the exact origin, not *, and the response should also include:
Access-Control-Allow-Credentials: true
Cookie-session fetch example:
const response = await fetch(`${SPALA_API_BASE_URL}/api/me`, {
credentials: "include",
headers: {
"X-CSRF-Token": csrfToken,
},
});
The CSRF header name, cookie attributes, and whether CSRF is required are project-specific. Confirm them from generated project docs before using cookie sessions.
Cookie-session checklist:
| Field | What to confirm | | --- | --- | | SameSite | Lax, Strict, or None; cross-site cookies require SameSite=None | | Secure | Required for cross-site HTTPS cookies | | Domain/path | Which frontend and API hosts receive the cookie | | CSRF source | Meta tag, cookie, endpoint, or generated page value | | Logout | Whether logout expires the cookie, revokes the server session, or both |
Realtime connection example
Realtime channel URLs are project-specific. Use the WebSocket URL shown by the project or documented in generated project docs. Do not assume the generated OpenAPI file contains the realtime protocol.
const wsUrl = new URL("/ws", SPALA_API_BASE_URL);
wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
wsUrl.searchParams.set("token", userJwt);
const socket = new WebSocket(wsUrl.toString());
socket.addEventListener("open", () => {
socket.send(JSON.stringify({
type: "subscribe",
channel: "cases",
}));
});
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
console.log("Spala realtime message", message);
});
The default examples use /ws because it is the common channel path, but the project channel settings win. If the project base URL includes a scoped path, or the channel uses a path such as /ws/chat, copy the exact WebSocket URL or path from the generated project docs. Do not strip scoped paths from shared-runtime URLs.
Common message envelope:
{
"type": "subscribe",
"channel": "cases"
}
Common event and error envelopes:
{
"type": "case.updated",
"channel": "cases",
"payload": {
"id": "case_123",
"status": "open"
}
}
{
"type": "error",
"code": "unauthorized",
"message": "Invalid or expired token"
}
Reconnect behavior, presence, history replay, and channel-specific payloads are project-defined. Confirm them from the generated project docs or channel configuration.
Owner realtime contract workflow:
1. Open Channels in the project dashboard. 2. Select the channel the frontend will use. 3. Copy the exact WebSocket URL or path, auth mode, permissions, subscribe envelope, event examples, errors, heartbeat interval, pong timeout, close codes, replay behavior, and reconnect expectations. 4. Attach that contract to the frontend handoff packet. 5. If the contract is missing, treat realtime integration as blocked until the owner supplies it.
Common realtime operational fields to include in a handoff:
| Field | Example |
| --- | --- |
| Heartbeat | Server sends { "type": "ping" }; client replies { "type": "pong" } |
| Close codes | 1000 normal, 1008 auth or policy failure, project-specific codes |
| Acknowledgement | { "type": "ack", "id": "msg_123" }, if implemented |
| Replay | Last-event-id, cursor, or no replay |
| Reconnect | Exponential backoff such as 1s, 2s, 5s, 10s, max 30s |
If no replay is implemented, refetch REST state after reconnect.
Mobile clients
Native mobile apps do not use browser CORS, but they still need the exact same project contract: API base URL, auth routes, token field, upload mode, realtime path, and publish environment. Store tokens in platform secure storage, not plain local storage. Confirm deep-link or callback URLs if the project uses OAuth providers. Reconnect websockets with backoff and refetch REST state after app resume.
Mobile implementation notes:
| Platform concern | Guidance | | --- | --- | | Token storage | Use iOS Keychain, Android Keystore, Expo SecureStore, or another platform secure store | | File uploads | Convert platform file URI/blob data to the request body expected by the project upload route | | OAuth callbacks | Confirm deep-link scheme, universal link, or app link callback URLs before release | | App resume | Treat resume as a possible stale-session event; refetch /api/me and resubscribe realtime channels | | WebSocket recovery | Reconnect with backoff and refetch REST state if replay is not guaranteed |
CORS and trusted origins
Browser frontends must be allowed by the project security settings when credentials or restricted origins are involved.
1. Open Settings → Security. 2. Find CORS Allowed Origins. 3. Add the exact frontend origin, for example https://app.example.com. 4. Save security settings. 5. Publish backend API changes before expecting saved security settings to affect the live published API.
Origins must include scheme and host. Do not add paths. For local development, add the exact dev origin such as http://localhost:5173.
If browser requests fail before reaching your endpoint, check DevTools for a CORS error, confirm the origin matches exactly, and verify that the frontend is calling the copied project API base URL.
For production, confirm the exact realtime auth contract in your project docs. Some projects use bearer headers through a client library, query tokens, cookies, or a project-specific handshake.
Realtime contract matrix
| Item | Rule | | --- | --- | | URL | Use the WebSocket URL or path generated for the project | | Browser auth | Browsers cannot set arbitrary WebSocket Authorization headers; use the documented project handshake | | Message shape | Use the channel event envelope from project API Docs | | Reconnect | Add reconnect/backoff logic in production clients | | Permissions | Confirm whether the channel uses public, JWT, API key, or project-specific auth |
Frontend quick verify
Before a frontend developer or AI coding agent starts implementation, give them:
1. Project API base URL. 2. Generated Markdown docs, OpenAPI JSON, or SDK. 3. Auth route and token response shape. 4. Upload endpoint contract if files are involved. 5. Realtime path and message envelope if live updates are involved. 6. Required frontend origin in Settings → Security → CORS Allowed Origins. 7. Confirmation whether public docs are intentionally enabled for this project or builder-authenticated docs must be used.
Use External Frontend Handoff when this information needs to be shared as one copyable packet.
Integration checklist
- API base URL copied from Lite Mode or Project Overview - Endpoint paths copied from Endpoints or API Docs - Auth routes tested in API Playground - Protected calls send Authorization: Bearer USER_JWT - Upload route contract confirmed before wiring file inputs - Realtime URL and auth handshake confirmed from project docs - VITE_SPALA_API_BASE_URL or NEXT_PUBLIC_SPALA_API_BASE_URL configured - CORS/trusted origins configured in project settings when browser calls are blocked
API Endpoints /endpoints Create and inspect REST routes External Frontend Handoff /guides/frontend-handoff Copyable packet for developers without dashboard access Protect Your Endpoints /endpoints/authentication JWT and protected endpoint behavior Upload Files /guides/file-uploads Build file upload endpoints Realtime Channels /channels/realtime Build realtime subscriptions CORS and Security /settings/cors-security Add frontend origins and publish security changes API Docs /features/api-docs Export OpenAPI and Markdown contracts