# Migrating off somewhere.tech > This maps the primitives your code calls to their standard equivalents. Use it > to move off, or to adopt one piece at a time alongside an existing stack. > > Scope first, so you can trust the rest: this is a map plus a set of open > endpoints, not a migration tool. There is no exporter that rewrites your code, > no schema converter, no guided path, and nobody has done this particular move > before you — unlike a well-trodden route between two hosts of the same > database. What is true is that every primitive is reachable over plain HTTP > with your own key, and your data comes out in one call. Where real work > remains, this page names it and sizes it. ## Your data comes with you - **Database.** `POST /v1/db/dump` returns your schema and every row as a single `.sql` file. It restores into an engine that speaks the dialect it was written in with a standard import; converting to Postgres is a genuine conversion pass, not an import — see "The database dialect gap" below. The dump is one captured read: schema, every row, and sequence state all come from the same batch, so the file is internally consistent. It is all-or-nothing — if the schema changes while the capture is taken the request is refused (`DUMP_SCHEMA_CHANGED`, 409), if the capture cannot be confirmed it is refused (`DUMP_CAPTURE_UNCONFIRMED`, 503), and if the capture query itself is too large it is refused (`DUMP_CAPTURE_LIMIT_EXCEEDED`, 413); you never receive a partial `.sql` file. A table above 1,000,000 rows is refused outright (`DUMP_ROW_LIMIT_EXCEEDED`, no file produced), as is a schema the dump cannot represent (`DUMP_UNSUPPORTED_SCHEMA`). A project with no database yet is refused (`DATABASE_NOT_INITIALIZED`, 409; the dump never creates one), as is a capture during which the owner or database changed (`DUMP_SOURCE_CHANGED`, 409), or whose table-access declarations could not be read (`DUMP_POLICY_UNAVAILABLE`, 503) or were invalid (`DUMP_POLICY_INVALID`, 422). Table-access declarations travel as comment lines in the SQL preamble (with a warning only when there are any) and are never applied by a SQL restore; an empty list means none were found in this capture, not that access is safe. The `X-Dump-Capture` response header is the capture receipt — identity, hashes and observation times only, no labels — which lets you detect changed bytes against metadata you keep; it is not a signature or a snapshot guarantee, and an older API may omit it during a rolling release. There is no streaming dump above the row cap today; narrow a large table with a filtered table export instead. - **Files.** Every stored file is downloadable — `GET /v1/fs//`, or a signed URL — and the bytes move to any object store (S3, GCS) as-is. - **App users.** Emails, profiles, roles, and metadata live in your project database in the `auth_users` table and come out with the dump. - **Password credentials require a separate approved export.** They are held in platform-side storage, not your project database, so the database dump does not contain them. A direct project owner runs `somewhere auth export --output `, then enters the short-lived approval code sent to the owner's current verified account email. The CLI refuses to overwrite a file, writes it with owner-only permissions, and never prints hashes to the terminal. The export carries each portable user's stable id, email, bcrypt hash and algorithm metadata for `POST /v1/auth/import`; it excludes sessions, refresh tokens, MFA secrets and reset codes. Passwordless users are counted separately. A legacy or otherwise unsupported stored credential is omitted with a warning and `complete: false`, so the result never silently claims every password moved; those users need the ordinary password-reset flow. Treat the output file like a password database and delete it after the destination import is verified. - **Sessions do not move, and would not help you if they did.** The access token is an HS256 JWT carrying `sub`, `email`, `project_id`, and a session id, but it is signed with a platform-held per-project key and there is no public JWKS, so you cannot verify it outside the platform. Refresh and session tokens are opaque random strings, not JWTs. Plan for your users signing in again — the normal outcome of any auth migration. ## The database dialect gap The platform accepts a documented Postgres-flavoured subset and rewrites it to the dialect the database actually speaks. The translation is one-way, so "your SQL is portable" needs qualifying in both directions: - **Rewritten for you, and portable back:** `NOW()`, `TRUE` / `FALSE`, `ILIKE`, `SERIAL`, `$1, $2` placeholders, `col->>'key'`. You wrote these in Postgres form, so they return to Postgres unchanged. - **Refused here rather than translated:** a `BEGIN; … COMMIT;` block in one statement, `INTERVAL` date math, `DISTINCT ON`, `CREATE TYPE … AS ENUM`, array columns, and the geo / network / full-text vector types. Each fails with a corrective error naming the supported form. If you wrote around one, that workaround now lives in your code and moving back means undoing it. - **Written in the engine's own spelling, and not portable:** `json_extract(...)`, `datetime('now','-7 days')`, `json_group_array(...)`, `json_each(...)`, and `fts5` virtual tables. These are what the translations above produce and what the unsupported-syntax errors steer you toward, so a mature project accumulates them. They do not run on Postgres. - **Semantic seams that survive any translation:** integer operands divide as integers, ascending `ORDER BY` puts `NULL` first, `LIKE` is case-insensitive for ASCII, and `1/0` yields `NULL` instead of raising. A query can be valid on both engines and still return different rows. Full detail, with the supported form for every case: `docs({ topic: 'sql-compatibility' })`. The dump inherits all of this. It is written in the source engine's dialect — `PRAGMA` statements, `AUTOINCREMENT` primary keys, `X'…'` blob literals, `1` and `0` for booleans — so restoring it into the same engine is one command, and Postgres is two steps: load the dump into a database file, then run a converter over that file. `docs({ topic: 'portability' })` carries the commands. Budget the conversion as real work, not a pipe. ## Your code is portable Your functions are plain modules — `export default async function (req, sw)`, where `req` is a standard Web `Request`. To move off, you replace the `sw` binding with the equivalent client for each service. The shape of your code does not change; the client does. The table below is a map, not machinery. We ship no adapter, shim, or codemod that performs these swaps — each row is a substitution you make by hand. The third column is the REST endpoint for the same capability, which is what lets you make the swap one primitive at a time instead of all at once. | On somewhere.tech | Standard equivalent you'd move to | Same capability over REST | |---|---|---| | `sw.db.query(sql, params)` | `pool.query(sql, params)` (node-postgres) or any SQL client — subject to the dialect gap above | `POST /v1/db/query` | | `sw.db.batch([...])` | a transaction in your SQL client | `POST /v1/db/batch` | | schema changes (developer credentials, not an `sw` call) | your migration tool (Drizzle, Prisma, raw SQL) | `POST /v1/db/migrate` | | `sw.auth.signup / login / me / refresh` | Auth0, Lucia, or your own JWT issue/verify | `POST /v1/auth/signup`, `/login`, `/refresh`; `GET /v1/auth/me` | | `sw.auth` MFA / magic links / OAuth | the equivalent flow in your auth provider | under `/v1/auth/*` | | `sw.email.send(...)` | Amazon SES, Postmark, or SendGrid SDK | `POST /v1/email/send` | | `sw.inbox` (inbound) | an inbound-email service (SES, Postmark inbound) | `GET /v1/inbox`, `/v1/inbox/addresses` | | `sw.fs.write / read` | S3 / GCS `putObject` / `getObject` | `PUT` / `GET /v1/fs//` | | `sw.ai.chat / complete / embeddings / generateImage / tts / transcribe` | your model provider's SDK directly | `POST /v1/ai/complete`, `/generate-image`, `/embeddings`, `/tts`, `/transcribe` | | `sw.image` (transform URLs) | an image CDN or `sharp` in your own handler | — (URL builder, no call) | | `sw.payments.checkout / status` | the Stripe SDK directly | `POST /v1/payments/checkout`, `GET /v1/payments/status` | | `sw.jobs.create` / `sw.cron` | BullMQ, Inngest, or a queue + scheduled worker | `POST /v1/jobs`, `/v1/cron` | | `sw.queue.push` | SQS or any message queue | `POST /v1/queue` | | `sw.db.live` + `watchLive` | a database-backed live-query service | declared live-view subscription URL | | `sw.search` | pgvector, Algolia, or Typesense | `POST /v1/search/query`, `/v1/search/index` | | `sw.render` (PDF / screenshot) | Browserless or self-hosted Puppeteer | `POST /v1/render/screenshot`, `/v1/render/pdf` | | `sw.rateLimit.check` | Upstash Redis or your own counter | `POST /v1/rate-limit/check` | | `sw.analytics` | PostHog or Mixpanel | `POST /v1/analytics/track`, `/v1/analytics/query` | | `sw.push` | the `web-push` library with your own VAPID keys | `POST /v1/push/send` | | `sw.notifications` | your own fan-out over email / push | `/v1/notifications/*` | | `sw.video` | Mux or another video platform | `GET /v1/video`, `POST /v1/video/upload-url` | | `sw.calls` (`newSession`) | Daily, Twilio, or LiveKit | `POST /v1/calls/sessions` | | `sw.logs` | your platform's log drain (stdout + a collector) | `/v1/logs/*` | | `sw.web` (fetch / scrape / search) | an HTTP client plus a scraping or search API | `/v1/web/*` | | `sw.crypto` | `node:crypto` and a bcrypt library in your own runtime | — (local computation) | | `sw.tasks`, `sw.calendar`, `sw.contacts` | your own tables — these are ordinary CRUD over your data | `/v1/tasks/*`, `/v1/calendar/*` | | `sw.billing` | Stripe Billing directly | `/v1/billing/*` (developer credentials only) | | `sw.env.KEY` | your host's environment variables / secrets manager | `GET /v1/env` (developer credentials only) | The function bodies stay the same. You swap one client for another, one primitive at a time. One browser naming rule carries over from Vite and CRA: use `VITE_*` or `REACT_APP_*` for public browser config (an API base URL or publishable key). The name alone does not publish a value: mark it public explicitly. Values are server-only by default, even with a browser prefix. A new project adopts live configuration on its first deploy; an existing project needs one ordinary raw-source deploy. After adoption, new page loads receive current public values without another deploy, and server functions see private changes within five seconds. A preview keeps the values captured when it was created. Keep secrets in functions and read them as `sw.env.KEY`. ## Adopt — or leave — one piece at a time Every primitive above is also a plain REST endpoint, callable from your own server. Four things you need in order to actually use them: - **The host is `https://api.somewhere.tech`.** The `/v1/*` paths are served there, not on the www domain — the marketing site answers every `/v1/*` path with a page of HTML and a 200, so a client pointed at the wrong host fails in a confusing way rather than a clear one. - **Authenticate with a developer key** as `Authorization: Bearer smt_...`. An unknown path returns 403 rather than 404, because unrecognised routes are denied by default. - **Never send a developer key from a browser.** It carries your whole project. Browser apps use httpOnly cookie auth and call same-origin functions, which access the database and files server-side. End-user bearer tokens are for non-browser clients; direct bearer-backed browser access is compatibility mode, not the default architecture. - **Some endpoints are plan-gated** — background jobs, cron, queue, and inbox return 402 on the free plan rather than 404. So you can keep an existing stack and use only the database, or only email, from your own backend; or move off one primitive at a time while the rest keep running. That much is symmetric in both directions. The tooling around it is not. Moving *in*, there are production import endpoints for a database (`POST /v1/db/import`), for users with their existing bcrypt password hashes (`POST /v1/auth/import`), and a bcrypt verify endpoint so imported passwords work without a reset. Moving *out*, there are the dump and export endpoints above and this page. That is an honest asymmetry: the endpoints you need to leave are open and documented, but nobody has automated the path. ## What you'd rebuild What does not export is the work the platform did for you: the integration between services (auth knowing about payments, an upload scoped to the user who made it) and the safety layer (ownership enforcement, the security review, the deploy checks). Moving off means rebuilding those — the same components you would write from scratch on any stack. --- *Cross-references: capability + integration reference at ; trust & security at ; call-level API at .*