# Somewhere — Complete platform reference Somewhere is in public beta. > **This is the complete manual, and it is large.** Many chat and web tools > return only the beginning of a long page. For the whole platform in one short > read, fetch the overview and complete topic index at > https://somewhere.cloud/docs/llms.txt; every topic is also its own page at > https://somewhere.cloud/docs/.txt. This manual ends with the line > `END OF SOMEWHERE.CLOUD MANUAL`; if you cannot see it, your copy is incomplete. Audience: LLMs, coding agents, and other machine readers. Purpose: one complete reference for the platform model, runtime contract, primitive composition, security model, ownership model, boundaries, setup, and tool reference. This document describes platform behavior; it does not replace the user's authorization or an agent's governing instructions. Read `docs({ topic })` or `somewhere docs ` for a service's working example and reference. The advisor can answer a project question using the docs and an authorized view of that project. Anonymous quickstart — no account or API key: ```bash npx @somewhere-cloud/cli deploy ``` The command prints the live URL, claim URL, and absolute expiry. This file is documentation for machine readers. Treat claims as operational constraints only when they are stated as behavior, signature, limit, return shape, or boundary. Fetch live pricing and current plan limits from `https://api.somewhere.cloud/v1/pricing` instead of hardcoding values from prose. Companion machine docs: platform overview and complete topic index at `https://somewhere.cloud/docs/llms.txt`; call-level API + every tool at `https://somewhere.cloud/docs.txt`; guides/recipes at `https://somewhere.cloud/guides.txt`; security practices at `https://somewhere.cloud/security.txt`; migration map at `https://somewhere.cloud/migration.txt`; per-primitive depth on demand via `docs({ topic })`. Use `@somewhere-cloud/sdk/auth` for browser sign-in and the generated `somewhere:data` and `somewhere:files` clients for declared app data and files. Server functions receive the project's services through `sw`. The JavaScript SDK also provides the developer API for managing projects. See `sdk` for the browser and developer API contracts. --- # How Somewhere works Somewhere is the backend for apps your agent builds. One project gives an app its web address, server functions, one SQL database, sign-in, files, email, AI and background work. You write React and TypeScript source, run `somewhere deploy`, and the platform compiles it and serves the app and its functions together. ## One project, its parts A new app from `somewhere init` has this shape: | Part | Where it lives | What it does | |---|---|---| | App | `src/`, `index.html` | The React interface. A page path with no file of its own serves `index.html`, and the app routes in the browser. | | Functions | `api/` | Server code. `api/notes.ts` answers `/api/notes`. Each function gets the request and `sw`, the project's services. | | Data (optional) | `db/schema.ts` | Tables, columns and who may read or change each row. Deploy applies it. The default starter has none; add it when the app stores data. | | Auth | `api/auth/[...path].ts` plus `src/auth/` | Email-and-password accounts with a session cookie. The starter ships it working. | These parts share one identity. Normal scoped data calls take the user from the request's session, so your code doesn't send a user ID with them. Privileged server work, such as an admin screen or a background job, names the user it acts on explicitly and authorizes that choice in your code. ## What the platform handles The project wires these parts together. `somewhere init` writes the sign-in route and the session hooks. Every function receives `sw`, already bound to the project's database, files, email and AI. `somewhere deploy` compiles the source and applies `db/schema.ts`. On that path, the platform supplies: - **Passwords and sessions.** Passwords are stored as salted hashes. Sign-in attempts are limited per source address and per account, with an account lockout after repeated wrong passwords. Sessions live in Secure, HttpOnly cookies that page JavaScript can't read. - **Cross-site request checks.** Cookie-authenticated writes from the browser must come from your app's own origin. A normal `fetch` from your page already sends it. - **Data access.** A table is closed to the browser until its declaration grants operations and columns. On an `owner()` table, normal operations act for the signed-in user. Rows get their owner from the verified session, never from browser input. - **Configuration and secrets.** New environment values are private by default and encrypted at rest. Once live configuration is in place, a changed key reaches new requests within 5 seconds, with no deploy. - **Outbound requests.** `fetch` calls from functions block private and metadata addresses, and redirects are checked again. - **Built-in services.** The database, auth, files, AI and transactional email are reached through `sw` without provider keys. Email has a test inbox for `*.test.somewhere.site` addresses. - **Deploys.** Raw source compiles on the platform, schema changes apply, and `somewhere verify` checks the live app in a real browser. ## What you decide - **Who may run a privileged action.** Check the caller in the function before it changes anything. Server-authority database calls skip row permissions on purpose, so they need your check. - **What's public.** Public reads, shared tables and public configuration values are explicit choices in your source. - **Effects that must not repeat.** A caller or a job can retry a request. Give actions such as sending email or charging a card an idempotency key, or make them safe to run twice. ## Design choices - **Data model:** one SQL database per project, scoped per user by declaration. Need Postgres for an existing dataset? Attach it. → `database-engine`, `postgres` - **Declared data:** schema and access rules live in one file that ships with the source, so the app, its tools and the platform read the same contract. → `declared-data` - **Secure by default:** scoped APIs carry the normal protections, and you add authorization to the privileged actions. → `security-model` - **Handled for you:** the full list of setup and operations the platform performs. → `handled-for-you` - **You can leave:** download a full database dump, your source and your users' accounts. → `portability` ## Reading these docs A primitive topic opens with a summary that lists its named sections. Ask for one section, or for the full reference, when you need more depth. A recipe returns the whole job on the first call. An explicit summary or section request still works for recipes too. ```bash somewhere docs sw.auth # first view somewhere docs sw.auth --section password-reset # one section ``` From MCP, use `docs({ topic: 'sw.auth', section: 'password-reset' })`. The full topic index lists every primitive, recipe and reference page with a line on when to load it. Read next: **Start an app**, then **Functions**, **Env vars and secrets**, **Data** and **Auth**. For questions about your own project, `somewhere advisor ""` answers with that project in view. --- # 1. Design premise If a backend were specified from zero for an application with end users, the records describing one user would not be split across unrelated systems. Identity, application data, payment state, email, files, notifications, and background work all describe the same actor and the same project. somewhere.cloud is specified as one backend namespace over those primitives. Consequences of that premise: - End-user identity and the application database share the same project boundary. - A signup creates a user record that application code can query and join. - Payment state is written back to the app-user record and project payment ledger by platform webhook handling. - Files can be scoped to the user and project that wrote them. - Email can be sent from the same runtime that knows the user and their payment/account state. - Background jobs, queues, cron handlers, live-view invalidations, push notifications, and analytics are bound to the same project identity. - Ownership checks can be enforced at the platform boundary instead of relying only on application-written predicates. - Deploy analysis can read the actual source using platform-specific knowledge of `sw.*` semantics. The breadth is intentional. The primitive set is the set of backend capabilities that normally need to coordinate around project identity, user identity, authorization, data ownership, delivery state, and billing state. The coordination is part of the platform surface, not an external integration project. --- # 2. Runtime mental model ## 2.1 Function signature A server function is a module with a default export: ```js export default async function (req, sw) { const body = await req.json(); const r = await sw.db.from('users', { where: { id: body.id }, limit: 1 }); return Response.json({ user: r.data[0] ?? null }); } ``` `req` is a standard Web `Request`. The function returns a standard Web `Response`. `sw` is the project-scoped platform binding. Database row operations use these fields: ```js r.data // array of row objects (empty array on writes) r.count // number of rows in r.data r.changes // rows affected by INSERT, UPDATE, or DELETE ``` Do not use `.rows` or `.results` for database return values. Composed calls (`sw.db.from`, `insert`, `update`, `remove`, `count`) resolve to a `Result` — `{ data, error }`, with `count`/`changes` on row operations — and `sw.db.count`'s number is `.data`. On the server, refusals and failures throw with a `code`; a result that is returned carries `error: null`. ## 2.2 Optional endpoint wrapper `sw.endpoint({ auth, body, rateLimit, handler })` is a declarative wrapper over the same function model. It can perform auth, body validation, rate limiting, CORS handling, and error formatting before invoking `handler`. The bare `export default async function(req, sw)` form remains valid for every function. ## 2.3 One binding The `sw` binding includes these primitive groups: - `sw.db` — relational database, migrations, batches, table metadata, scoped queries, export, and change webhooks. - `sw.auth` — end-user signup, login, session validation, refresh, password reset, email verification, magic links, OAuth, visitor sessions, user admin operations and lifecycle webhooks. - `sw.fs` — object/file storage, versioning, signed URLs, uploads, search, diffs, moves, copies, restores, and public URLs. - `sw.email` — outbound email send and delivery event visibility. - `sw.inbox` — inbound email addresses, message listing, message retrieval, attachments, replies, sending from inbox addresses, threads, and inbox rules. - `sw.ai` — chat, embeddings, transcription, TTS, image generation, moderation, tool loops, conversation history, structured output, model catalog, and per-user memory. - `sw.agent` — canonical inline and durable multi-step model/tool execution, checkpoints, status, and cancellation. - `sw.payments` — checkout, onboarding, plan tracking, refunds, cancellations, transactions, billing portal, and event ledger. - `sw.billing` — define an app's plans and the features each grants, then gate by a stable feature name (`await sw.billing.has(feature)` on the server; `` for presentation); the plan a user is on is set by the payments webhook automatically. - `sw.jobs`, `sw.queue`, `sw.cron` — durable background jobs, fire-and-forget work, and scheduled functions. - `sw.push`, `sw.notifications` — web-push delivery to already-registered subscriptions and notification fan-out helpers. Live updates to a browser come from `sw.db.live` declared views; there is no caller-named channel API in a function. - `sw.search` — semantic and full-text search surfaces. - `sw.render`, `sw.web`, `sw.image` — screenshots, PDFs, public web reading, and image transforms. - `sw.rateLimit`, `sw.analytics`, `sw.logs`, `sw.env`, `sw.tasks` — rate-limit counters, event analytics, runtime logs, project environment values (server-only by default; explicitly public browser-prefixed values compile into browser code), and per-project ticket/task state. ## 2.4 Internal and external access Inside a deployed function, use `sw.*`. The binding is already scoped to the project. No developer key is required inside runtime code. Outside a deployed function, use one of the external surfaces: - MCP tools at `https://mcp.somewhere.cloud/mcp`. - CLI commands through the `somewhere` binary. - REST API paths under `/v1/*` with an appropriate developer key or app-user JWT. Do not call `https://api.somewhere.cloud/v1/*` from inside a deployed function when an equivalent `sw.*` primitive exists. Runtime code should use the binding. ## 2.5 Deploy input Deploy raw source. Valid inputs include `.ts`, `.tsx`, `.js`, `.jsx`, `.html`, `.css`, static assets, and function files under `api/` or other documented function paths. The platform compiles your source at deploy time. Prebuilt output still deploys, with a warning, and loses source round-trips (export, find/replace patches, design tokens, the visual editor); `somewhere deploy --prebuilt`, the project setting, or `allow_bundled: true` accepts that trade-off and drops the warning. ## 2.6 Runtime transparency and networking Functions run in a Workers-compatible Web runtime, not a Node server process. The compiler passes `node:*` standard-library compatibility imports and `cloudflare:workers`; common `node:*` modules include `crypto`, `buffer`, `events`, `path`, `stream`, `string_decoder`, `util`, and `url`. Standard Web APIs include Request/Response/Headers, URL/URLSearchParams, fetch, WebSocket, crypto, text encoders, streams, Blob/FormData, timers, and AbortController. Plain `fetch()` reaches public HTTP and HTTPS hosts; the runtime refuses a destination in a private, loopback, link-local or metadata range, as written (`SSRF_BLOCKED`) or as resolved (`DNS_LOOKUP_FAILED`), and re-checks each redirect it follows. Behind it, the platform's outbound guard refuses a request that reaches it with a literal blocked target (non-public address or internal hostname, embedded credentials, non-HTTP scheme, port other than 80, 443 or 1024 and above) with a 403 before it leaves. `sw.fetch()` adds resolved-address checks before connecting, redirect validation, bounded timeouts/redirects, structured results, logging, and safe idempotent retries; resolution is checked at request time, not continuously. Raw TCP sockets and listening ports are not part of the customer runtime contract; use HTTPS or an outbound WebSocket. ## 2.7 Verify before deploy Use local `somewhere typecheck` for TypeScript; `somewhere deploy-check` for the real server-side compiler; `somewhere run ``` Put the preview image at `public/og.png` and reference it by its full URL: link-preview services don't resolve relative image paths. A 1200×630 image fits the large preview card. ## 2. A different preview for a shareable page Link-preview services read the HTML without running your JavaScript, so every route of a single-page app shares the tags in `index.html`. For a page that people share on its own, such as `/pricing`, ship an HTML file at `pricing/index.html` with its own head and the same app script: ```html Pricing — Notes
``` The deploy compiles every HTML page's app script, so `/pricing` runs the same app and your router renders the pricing route. The generated sitemap lists `/pricing` too. For routes that only exist inside the client router, set the tab title as the route renders: ```tsx // src/pages/PricingPage.tsx import { useEffect } from 'react'; export function PricingPage() { useEffect(() => { document.title = 'Pricing — Notes'; }, []); return

Pricing

; } ``` ## 3. Sitemap and robots The generated sitemap lists each deployed HTML page with its upload date. Routes that exist only inside the client router aren't deployed pages, so it can't list them: give them their own HTML file (step 2) or ship your own `public/sitemap.xml`. Shipping `public/robots.txt` or `public/sitemap.xml` replaces only that generated file. Your rules are served as written, including `noindex` or `Disallow` rules. ### Change notification (IndexNow) After a release is live, the platform tells the IndexNow search engines (Bing and the others that share IndexNow submissions) which public pages are new or changed since the previous live release. Nothing needs to be configured. - **Which pages:** deployed HTML pages, by the same rule as the generated sitemap. A page counts when its file is new or its bytes changed; at most 1,000 URLs are sent per release. When the previous release can't be read, every public page is sent. - **Which hosts:** one submission per serving host, meaning the project's `.somewhere.site` address and each attached custom domain. The dev slot's address is never sent. - **Ownership key:** each serving host answers `/.txt` with the project's IndexNow key, which the engines fetch to confirm the host. The key is derived from the project and grants no access to it. A file you deploy at that path is served instead. - **Nothing is sent** for temporary (unclaimed) or test projects, for draft, frozen, archived or deleting projects, for a release that a newer release or a rollback has already replaced, when `robots.txt` disallows every crawler (`User-agent: *` with `Disallow: /`), or when no public page changed. - **Errors never block a deploy.** An engine that is slow or refuses the submission changes nothing about the release; the attempt is logged. A submission asks the engines to recrawl those URLs. It is not indexing: whether and when a page appears in results is up to each engine. Google does not use IndexNow; Google Search Console handles sitemap submission for Google. ## 4. Check the live release After a deploy, read what the active release ships: ```text project_seo_check({ project_id: "my-app" }) ``` It inspects the deployed `index.html`, `robots.txt` and `sitemap.xml` of the active release, without fetching the site. Each item has a `status`: - `automatic`: the release uses the platform fallback configuration. - `app-supplied`: the release contains your file. - `needs-attention`: something to fix, such as a missing title or description, with a `suggestion`. - `unknown`: the check couldn't inspect this item. Open the public URLs to check the served pages and files. The same report is on the project's Home tab in the dashboard. Search Console verification and sitemap submission are done in Search Console. ## 5. Server-rendered pages and redirects For database-backed articles, return HTML from a root `[...path].ts` handler. The `functions` topic's server-rendered pages recipe shows a public article route alongside a React editor. Return each article's title, description, canonical URL and body in that HTML; link previews do not run browser JavaScript. A page handler can also return `Response.redirect(url, 301)`. A client-only React route still renders in the browser. Use the static HTML approach above for fixed pages, or the HTML handler for dynamic content. Generated sitemap and IndexNow discovery cover deployed HTML files; they do not enumerate database-backed article URLs. Supply a sitemap containing those public URLs and check its served response alongside the articles. ## Deployed SEO metadata `project_seo_check({ project_id })` and `GET /v1/projects/:id/seo` return the same authenticated, read-only inspection shown in the Summary section of the project's Home tab. The response identifies `release_id`, `version`, `source_manifest_hash` and `compiled_artifact_digest` for the exact active release. It inspects only `index.html`, `robots.txt` and `sitemap.xml`; missing, changed or unverifiable release evidence is unavailable, not a pass. Each item carries `status` (`automatic`, `app-supplied`, `needs-attention`, `unknown`), `provenance`, bounded `values`, a `value_count`, `values_truncated`, a `note` and optional advisory `suggestion`. At most five values of 512 characters are returned per item; duplicate detection retains the full count. Automatic means a platform fallback is configured because an app-supplied file is absent; read the public URL to confirm a request serves it. App-supplied robots rules and metadata, including intentional noindex or Disallow rules, are preserved. No source is edited. Inspecting a file larger than 256 KiB returns unknown. Title, description, canonical and Open Graph values come from the deployed homepage HTML. Static body text there reflects the file only: check the rendered page for what is visible; crawlability stays unverified. `coverage.public_response` is `unobserved`: no HTTP fetch, function execution, authentication check, JavaScript rendering or crawl occurs. Client routes, indexing and ranking remain unknown. Generated sitemaps include static HTML paths only, not routes hidden behind a client router. Search Console verification and submission are separate from this inspection. ## Automatic sitemap and robots files If a project does not ship its own `sitemap.xml` or `robots.txt`, the platform serves both automatically. The sitemap lists deployed HTML pages and uses their upload times for ``; robots points crawlers at that sitemap. Shipping either file overrides only that generated fallback. Routes that exist only inside a client-side router are not separate deployed HTML pages, so the generated sitemap cannot discover them. Ship crawlable HTML for those routes or provide your own `sitemap.xml`. ## Going further | Need | Where | |---|---| | A custom domain for the canonical URL | `deploy` → `connect-a-domain`; `domains` | | How the check reports each field | `recipe-seo` → `deployed-seo-metadata` | | The generated sitemap and robots files | `recipe-seo` → `automatic-sitemap-and-robots-files` | | Visitor counts and top pages | `analytics` → `page-views` | --- ## Topic: Recipe — CSV import (recipe-csv-import) Page: https://somewhere.cloud/docs/recipe-csv-import.txt > **Not yet verified end to end:** the job's database reads and writes in this > recipe have been run; reading the uploaded file and finishing an import have > not. If an import's job fails, the status read in section 3 reports that > failure instead of leaving the import `queued`. A signed-in user uploads a CSV of contacts and sees how many rows were stored. The browser sends the file straight to storage, one endpoint starts a background job, and the job validates every row, then writes them in atomic batches. A retried start, a repeated delivery and two overlapping runs all leave each row stored once, for that user only. ## 1. db/schema.ts ```ts // db/schema.ts import { files, id, integer, owner, schema, table, text, timestamp } from 'somewhere/db'; export default schema({ imports: table( { id: id(), file_id: text({ unique: true }), status: text({ default: 'queued' }), job_id: text({ nullable: true }), rows_stored: integer({ default: 0 }), error: text({ nullable: true }), created_at: timestamp({ default: 'now' }), }, { scope: owner(), client: { read: ['id', 'file_id', 'status', 'rows_stored', 'error', 'created_at'] } }, ), contacts: table( { id: id(), row_key: text({ unique: true }), import_id: text(), name: text(), email: text() }, { scope: owner(), indexes: [['import_id']], client: { read: ['id', 'name', 'email'] } }, ), }, { files: { csv_uploads: files({ path: '/csv-uploads', scope: owner(), client: { read: true, upload: true }, limits: { maxSize: '2MB', types: ['text/csv'] }, }), }, }); ``` `owner()` on all three gives each user their own uploads, import rows and contacts. The browser can upload and read; only your functions write rows. `row_key` is `:`, so a row written twice is the same row. The collection accepts no browser replace or delete, so the file a job reads is the file the user uploaded. The page stores every upload as `upload.csv` under its own file id, so the functions build the file's path from the id alone, never from a name the browser sends. `'2MB'` is this example's cap; your plan's per-file ceiling is in `limits`. ## 2. The CSV reader ```ts // api/_lib/csv.ts — comma-separated text: quoted fields, "" escapes, commas and line breaks inside quotes, LF or CRLF export class CsvError extends Error {} /** maxRecords counts the header row. */ export function parseCsv(text: string, maxRecords: number): string[][] { const records: string[][] = []; let record: string[] = []; let field = ''; let state: 'start' | 'plain' | 'quoted' | 'closed' = 'start'; const endField = () => { record.push(field); field = ''; state = 'start'; }; const endRecord = () => { endField(); if (record.length > 1 || record[0] !== '') records.push(record); // skip blank lines if (records.length > maxRecords) throw new CsvError(`The file has more than ${maxRecords - 1} rows below the header.`); record = []; }; for (let i = text.charCodeAt(0) === 0xfeff ? 1 : 0; i < text.length; i++) { const ch = text[i]; if (state === 'quoted') { if (ch !== '"') field += ch; else if (text[i + 1] === '"') { field += '"'; i++; } else state = 'closed'; } else if (ch === ',') endField(); else if (ch === '\n') endRecord(); else if (ch === '\r' && text[i + 1] === '\n') { endRecord(); i++; } else if (ch === '"' && state === 'start') state = 'quoted'; else if (ch === '"' || ch === '\r' || state === 'closed') throw new CsvError(`Row ${records.length + 1} is not valid CSV.`); else { field += ch; state = 'plain'; } } if (state === 'quoted') throw new CsvError('A quoted field is not closed.'); if (state !== 'start' || field !== '' || record.length > 0) endRecord(); return records; } ``` A file under `api/_lib/` is bundled into the functions that import it and is never served as a route. Values come back as strings, unchanged. ## 3. Start the import and report it ```ts // api/imports.ts — POST { file_id } starts the import of one uploaded CSV; GET ?file_id= reports it export default async function imports(request: Request, sw: SomewhereRuntimeContext): Promise { const user = await sw.auth.fromRequest(request); if (!user) return Response.json({ error: 'Sign in.' }, { status: 401 }); if (request.method === 'GET') { const fileId = new URL(request.url).searchParams.get('file_id'); if (!fileId) return Response.json({ error: 'Pass ?file_id=.' }, { status: 400 }); const readRow = async () => (await sw.db.from('imports', { where: { file_id: fileId }, limit: 1 })).data[0]; const row = await readRow(); // runs as this user: another user's import is not found if (!row) return Response.json({ error: 'No such import for this account.' }, { status: 404 }); const recorded = (r: NonNullable) => Response.json({ status: r.status, rows_stored: Number(r.rows_stored), error: r.error }); if (row.status !== 'queued' || typeof row.job_id !== 'string') return recorded(row); // Still queued: the job's own status tells a run that ended apart from one still going. const job = await sw.jobs.status(row.job_id); // only the user who started the job can read it if (job.status === 'failed' || job.status === 'cancelled') { return Response.json({ status: 'failed', rows_stored: Number(row.rows_stored), error: job.error ?? `The import job was ${job.status}.`, error_code: job.error_code ?? null, }); } if (job.status === 'complete') { const latest = await readRow(); // the job records its result before it finishes if (!latest) return Response.json({ status: 'unavailable', rows_stored: null, error: 'The import could not be read. Check again later.' }); if (latest.status !== 'queued') return recorded(latest); // Finished without recording a result. Some contacts may already be stored, so this is not a clean failure. return Response.json({ status: 'unrecorded', rows_stored: Number(latest.rows_stored), error: 'The import finished without recording its result. Check the stored contacts before importing this file again.', }); } // queued, scheduled, running or indeterminate: not finished, so never reported as failed. return Response.json({ status: 'queued', rows_stored: Number(row.rows_stored), error: null, job_status: job.status, recovery_error: job.recovery?.recovery_error ?? null, }); } if (request.method !== 'POST') return new Response('Use GET or POST.', { status: 405, headers: { Allow: 'GET, POST' } }); const input = (await request.json().catch(() => null)) as { file_id?: unknown } | null; const fileId = input?.file_id; if (typeof fileId !== 'string' || !/^[A-Za-z0-9-]{8,64}$/.test(fileId)) { return Response.json({ error: 'Send the file_id from the upload result.' }, { status: 400 }); } // 20 per hour per user is this example's choice. const allowance = await sw.rateLimit.check(`csv-import:${user.id}`, 20, 3600); if (!allowance.allowed) return Response.json({ error: 'Try again later.' }, { status: 429 }); try { await sw.fs.stat(`/csv-uploads/${fileId}/upload.csv`); // runs as this user: another user's file is not found } catch (error) { if (error instanceof Error && 'status' in error && error.status === 404) { return Response.json({ error: 'No such upload for this account.' }, { status: 404 }); } throw error; } await sw.db.insert('imports', { file_id: fileId }, { onConflict: 'ignore' }); // Same file → same key and payload → the same job, never a second one. // user_id comes from the verified session, never from the request body. const job = await sw.jobs.create({ handler: '/api/jobs/import-csv', idempotency_key: `csv-import:${fileId}`, payload: { file_id: fileId, user_id: user.id }, }); // Keep the job handle on the user's import row, so the status read can report a job that ended. await sw.db.update('imports', { where: { file_id: fileId }, set: { job_id: job.job_id } }); return Response.json({ file_id: fileId }, { status: 202 }); } ``` Starting the job and storing its id on the import row are two separate calls, not one atomic step. If storing the id fails, the request answers an error even though the job may already be admitted and running. Sending the same `file_id` again reaches that same job, because the key and payload are the same, and stores its id. Nothing here guarantees the job runs exactly once; the job itself is written to be safe to repeat. ## 4. The job ```ts // api/jobs/import-csv.ts — runs only when the platform delivers the job import { CsvError, parseCsv } from '../_lib/csv'; const MAX_ROWS = 2000; // this example's cap: one run reads, checks and writes the whole file const BATCH = 100; // sw.db.tx takes 1–100 writes export default async function importCsv(request: Request, sw: SomewhereRuntimeContext): Promise { if (!(await sw.jobs.verifyInvocation(request))) return Response.json({ error: 'forbidden' }, { status: 403 }); const payload = (await request.json().catch(() => null)) as { file_id?: unknown; user_id?: unknown } | null; const fileId = payload?.file_id; const userId = payload?.user_id; // 410 tells the platform not to deliver this job again. if (typeof fileId !== 'string' || !/^[A-Za-z0-9-]{8,64}$/.test(fileId) || typeof userId !== 'string' || !userId) { return Response.json({ error: 'Invalid payload.' }, { status: 410 }); } // Server authority, scoped by hand to the user the start request put in the payload. const { data: found } = await sw.db.server.from('imports', { where: { file_id: fileId, user_id: userId }, limit: 1 }); const job = found[0]; if (!job || job.status === 'complete' || job.status === 'failed') return Response.json({ ok: true }); const fail = async (message: string) => { await sw.db.server.update('imports', { where: { file_id: fileId, user_id: userId, status: 'queued' }, set: { status: 'failed', error: message } }); return Response.json({ ok: false }); }; // Plain sw.fs runs as the user who started the job: their upload, nobody else's. const file = await sw.fs.read(`/csv-uploads/${fileId}/upload.csv`); let records: string[][]; try { const text = new TextDecoder('utf-8', { fatal: true }).decode(await file.arrayBuffer()); records = parseCsv(text, MAX_ROWS + 1); } catch (error) { if (error instanceof CsvError) return fail(error.message); if (error instanceof TypeError) return fail('Save the file as CSV UTF-8 and upload it again.'); throw error; } const [header, ...rows] = records; if (header?.map((h) => h.trim().toLowerCase()).join(',') !== 'name,email') return fail('The first row must be: name,email'); for (const [index, row] of rows.entries()) { const [name = '', email = ''] = row.map((value) => value.trim()); if (row.length !== 2 || !name || name.length > 200 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return fail(`Row ${index + 2}: expected a name and an email address.`); } } for (let start = 0; start < rows.length; start += BATCH) { await sw.db.server.tx(rows.slice(start, start + BATCH).map((row, offset) => ({ op: 'insert' as const, table: 'contacts', values: { row_key: `${fileId}:${start + offset + 2}`, import_id: fileId, name: row[0]!.trim(), email: row[1]!.trim(), user_id: userId }, options: { onConflict: 'ignore' as const }, }))); } const { data: stored } = await sw.db.server.count('contacts', { where: { import_id: fileId, user_id: userId } }); await sw.db.server.update('imports', { where: { file_id: fileId, user_id: userId, status: 'queued' }, set: { status: 'complete', rows_stored: stored } }); return Response.json({ ok: true }); } ``` The job reads the file with plain `sw.fs`, which runs as the user who started the job, so it can only open that user's upload. Its database calls use `sw.db.server` and name `user_id` on every read and write: plain `sw.db` in a job is refused with `ROW_JOB_REQUIRED`, because a job acts as its user on managed data only through a one-row binding (`sw.jobs` → `act-on-one-row-for-the-signed-in-user`). The `user_id` is safe to trust because the start request took it from the session and the platform delivers the stored payload with a signature; a direct call to the job fails `verifyInvocation`. The job checks every row before it writes any, so a bad file stores nothing and the import row says which row to fix. Each batch is atomic, and `onConflict: 'ignore'` makes a batch that already landed write nothing the second time. A failure the job returns (bad header, invalid row, not UTF-8) answers 200, so the platform does not retry it; an error it throws, such as a lost connection, is retried, and the retry resumes safely. If every retry throws, the job ends `failed` while the import row is still `queued`; the status read in section 3 reports the job's `error` and `error_code` for that import, so the page never waits on it. Batches that landed before the failure stay stored. A run that has not answered within the job's `timeout_seconds` is not sent again, and the platform never starts a second delivery while one may still be running. A retry after an error the handler returned still repeats earlier batches, and the unique `row_key` makes that harmless. Both final updates are guarded by `status: 'queued'`, so a late duplicate run never overwrites a finished import. `rows_stored` is counted from the table, not added up, so it is right after any number of repeats. ## 5. The page ```ts // src/services/csv-import.ts import { files } from 'somewhere:files'; export type ImportState = { status: string; rows_stored: number | null; error: string | null; error_code?: string | null; recovery_error?: string | null; }; /** Upload a CSV and start its import. Returns the file id to check on. */ export async function startCsvImport(file: File): Promise { if (!file.name.toLowerCase().endsWith('.csv')) throw new Error('Choose a .csv file.'); const { data: uploaded, error } = await files.csv_uploads.upload(file, { name: 'upload.csv', contentType: 'text/csv' }); if (error) throw new Error(error.message); const response = await fetch('/api/imports', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ file_id: uploaded.id }), }); if (!response.ok) throw new Error(`Could not start the import (${response.status}).`); return uploaded.id; } /** Read one import's progress, reconciled with its job. */ export async function readCsvImport(fileId: string): Promise { const response = await fetch(`/api/imports?file_id=${encodeURIComponent(fileId)}`); if (response.status === 404) return null; const result = (await response.json().catch(() => null)) as ImportState | null; if (!response.ok || typeof result?.status !== 'string') throw new Error(`Could not read the import (${response.status}).`); return result; } ``` Browsers label `.csv` files differently, so the page checks the extension and sends the declared `text/csv` type itself, under the fixed name `upload.csv`. The job still rejects anything that is not valid UTF-8 CSV. Call `startCsvImport` from a file input and `readCsvImport` from a **Check status** button: - `queued` means the job has not finished. A `recovery_error` with it is still not final: check again later rather than starting the same import again. - `complete` shows `rows_stored`. - `failed` shows `error`. When the job recorded the failure (a bad header, an invalid row, not UTF-8), it stored nothing, so upload the corrected file; it gets a new file id. When the job itself failed (`error_code` is set), some batches may already be stored: check the contacts before importing the file again. - `unrecorded` means the job finished without recording its result, so some contacts may be stored. Check them before importing the file again. - `unavailable` means the import could not be read just now. Check again later. ## 6. Check it ```bash somewhere typecheck && somewhere deploy somewhere db query "SELECT user_id, import_id, COUNT(*) AS rows FROM contacts GROUP BY user_id, import_id" ``` Upload a three-row file: the import shows `complete` and `rows_stored: 3`, and the query shows three rows for that user. Upload a file with a missing email on row 3: it shows `failed` with `Row 3: expected a name and an email address.` and stores nothing. A second user sees none of the first user's imports or contacts. ## Larger files Each run of a job is one function request under the request limits in `limits`. This recipe's caps (2 MB, 2,000 rows) keep one run's read, check and 20 batched writes small. Raise them only after timing a full run on your deployed function. To load a dataset once as the developer, not from the app, use the `db_import_csv` tool with a CSV already in your project files. --- ## Topic: Recipe — PDFs (recipe-pdfs) Page: https://somewhere.cloud/docs/recipe-pdfs.txt Write the document as HTML and CSS, and `sw.render.pdf` prints it in a real browser: web fonts, flexbox, tables and page breaks render the way they do on screen. This recipe makes an invoice for the signed-in user, stores it as a private file, previews it in the page and merges PDFs in a function. ## 1. Generate and store an invoice ```ts // db/schema.ts import { id, integer, owner, schema, table, text } from 'somewhere/db'; export default schema({ invoices: table( { id: id(), number: text(), customer: text(), amount_cents: integer() }, { scope: owner(), client: { read: ['id', 'number', 'customer', 'amount_cents'] } }, ), }); ``` ```ts // api/invoices/[id].ts — POST renders the invoice PDF; GET returns it const escape = (value: string) => value.replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch]!); export default async function invoicePdf(request: Request, sw: SomewhereRuntimeContext): Promise { const user = await sw.auth.fromRequest(request); if (!user) return Response.json({ error: 'Sign in.' }, { status: 401 }); const id = new URL(request.url).pathname.split('/').pop() ?? ''; if (!/^\d+$/.test(id)) return Response.json({ error: 'Unknown invoice.' }, { status: 404 }); const path = `/invoices/${user.id}/${id}.pdf`; if (request.method === 'GET') { try { return await sw.fs.server.read(path); // streams the stored PDF with its content type } catch (error) { if (error instanceof Error && 'status' in error && error.status === 404) { return Response.json({ error: 'Generate the PDF first.' }, { status: 404 }); } throw error; } } if (request.method !== 'POST') return new Response('Use GET or POST.', { status: 405, headers: { Allow: 'GET, POST' } }); // owner(): only this user's invoice is found. const { data } = await sw.db.from('invoices', { where: { id }, limit: 1 }); const invoice = data[0]; if (!invoice) return Response.json({ error: 'Unknown invoice.' }, { status: 404 }); const html = `

Invoice ${escape(invoice.number)}

Billed to${escape(invoice.customer)}
Total$${(Number(invoice.amount_cents) / 100).toFixed(2)}
`; const pdf = await sw.render.pdf({ html, format: 'A4', print_background: true }); await sw.fs.server.write(path, await pdf.arrayBuffer(), { contentType: 'application/pdf' }); return Response.json({ path }, { status: 201 }); } ``` `sw.render.pdf({ html })` returns the PDF as a `Response`. `sw.fs.server.write` stores it as a private file, so only your functions read it. The path holds the signed-in user's id, taken from the session, so one user's request can never name another user's file. Escape every value you put into the HTML. Render once and store the file; serve the stored copy after that. Options: `format` is `'A4'`, `'A3'`, `'Letter'` (the default), `'Legal'` or `'Tabloid'`; `landscape: true` rotates the page; `print_background: true` prints background colours and images; `wait_for: ''` waits up to 5 seconds for an element before printing. Pass `url` instead of `html` to print a public page. `html` is at most 1,000,000 characters, and the page must finish loading within 30 seconds. Renders are metered and charged to the account balance. ## 2. Preview it in the page Load the stored PDF from your own function, same origin, and draw it with PDF.js. It draws to a canvas, so the preview looks the same on phones and desktops. ```tsx // src/ui/InvoicePreview.tsx — npm install pdfjs-dist (version 5 or later) import { useEffect, useRef, useState } from 'react'; import * as pdfjs from 'pdfjs-dist'; pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`; export function InvoicePreview({ invoiceId }: { invoiceId: string }) { return ; } function InvoicePreviewPage({ invoiceId }: { invoiceId: string }) { const canvas = useRef(null); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; let renderTask: { cancel(): void } | null = null; const loadingTask = pdfjs.getDocument({ url: `/api/invoices/${invoiceId}` }); (async () => { const doc = await loadingTask.promise; const page = await doc.getPage(1); const target = canvas.current; if (cancelled || !target) return; const viewport = page.getViewport({ scale: 1.5 }); target.width = viewport.width; target.height = viewport.height; const task = page.render({ canvas: target, viewport }); renderTask = task; await task.promise; })().catch((reason: unknown) => { if (cancelled) return; // a replaced or unmounted preview reports nothing setError(reason instanceof Error ? reason.message : 'Could not load the PDF.'); }); return () => { cancelled = true; renderTask?.cancel(); // cancels the previous render loadingTask.destroy().catch(() => {}); // releases the loading task and worker }; }, [invoiceId]); return ( <> {error &&

{error}

}