Frontends. Vercel, Netlify and a frontend in another repository.
Keep your frontend previews where they are. Karts gives each branch its own backend, with its own migrated and seeded Postgres, at https://<env>.karts.kartikey.fyi, and your preview calls it. If the frontend is itself a Karts project in another repository, use links: instead.
Status. The CLI output and allow_origins below are built and tested. The GitHub Actions workflow that runs this per pull request is tested against fakes only. It has not yet been run against real Vercel or Netlify projects, so it is not published here yet.
1. A same-origin rewrite (recommended)
The frontend host proxies /api/* to the backend. The browser sees one origin: no CORS, and a cookie the backend sets is first-party on the preview's host.
Next.js on Vercel. Next reads rewrites() at build time, so set KARTS_BACKEND_URL as a branch-scoped Preview variable before the build:
// next.config.mjs: /api/* on the preview goes to the branch's Karts backend
export default {
async rewrites() {
const url = process.env.KARTS_BACKEND_URL; // the branch's Karts URL
return url ? [{ source: "/api/:path*", destination: `${url}/api/:path*` }] : [];
},
};
Netlify. A _redirects rule, written into the built site at deploy time:
/api/* https://feature-x-1a2b.karts.kartikey.fyi/api/:splat 200!
A plain vercel.json cannot read a variable, so it only works for one fixed backend.
2. Cross-origin calls with allow_origins
List the frontend origins in the backend's karts.yml. Karts validates them and passes them to your app as KARTS_ALLOWED_ORIGINS, canonical and comma-separated. Your app answers CORS: Karts' proxy passes CORS headers through and adds none.
allow_origins: - https://shop-*-myteam.vercel.app # Vercel previews: project shop, team myteam - https://*--mysite.netlify.app # Netlify deploy previews of site mysite - http://localhost:3000 # a local frontend # or: allow_origins: [https://shop-*-myteam.vercel.app, http://localhost:3000]
- Each pattern is
scheme://host[:port], lower case, with nothing after the host.httpsfor every host;httponly forlocalhost, hosts ending in.localhost(such ashttp://*.shop-web.localhost:7400, every local branch of shop-web) and127.0.0.1. - One
*at most, inside the leftmost label. It matches one or more letters, digits and dashes, never a dot. - Refused:
*,null, IP addresses other than127.0.0.1, a whole shared-hosting domain such ashttps://*.vercel.apporhttps://*.netlify.app(anyone can deploy there), a wildcard right under a public suffix such asco.uk, and any wildcard underkarts.kartikey.fyi. At most 20 patterns. A bad pattern failskarts upwith the line and the fix. KARTS_ALLOWED_ORIGINSholds the patterns as you wrote them. A CORS library that compares origins exactly, such as Expresscorsor NestJSenableCorsgiven a list, never matches a pattern with*and sends noAccess-Control-Allow-Origin. With a wildcard, pass the library a function that applies the rules above, or list exact origins. Withkarts up --local, Karts also appends the exact origin of each local environment a wildcard matches at thatup, so an exact-match library works there. A front end started later is added at the API's nextkarts up --local.- Answer with the request's
OrigininAccess-Control-Allow-Origin, plusVary: Origin. Never*, and neverAccess-Control-Allow-Credentials: send a bearer token inAuthorizationinstead. - CORS decides what a browser page may read. It is not authentication:
curlignores it. Karts environments are public and hold seed data only.
Cookies
- A cookie your backend sets on a cross-origin call is a third-party cookie. Safari and Firefox block those by default, so do not build on it. Use the rewrite, or bearer tokens.
- Never set
Domain=karts.kartikey.fyion a cookie. Every environment shares that domain for now, so such a cookie would reach all of them. Host-only cookies, with noDomain, are the safe kind.
3. The backend URL in CI
In CI, pass --branch and --workspace explicitly, and --commit to up. A fresh clone has its own random workspace, so a later karts url or karts down would not find the environment otherwise.
URL=$(karts up --commit "$SHA" --branch "$BRANCH" --workspace "gh-pr-$PR" --output url) karts url --branch "$BRANCH" --workspace "gh-pr-$PR" # the same URL again, later karts down --branch "$BRANCH" --workspace "gh-pr-$PR" --output json
karts up --output json prints one JSON object on stdout and nothing else:
{
"schema_version": 1,
"env": "ev_…",
"name": "feature-x-1a2b",
"key": {"project": "pr_…", "branch": "feature/x", "workspace": "gh-pr-42", "slot": 1},
"url": "https://feature-x-1a2b.karts.kartikey.fyi",
"generation": 3,
"state": "ready",
"created": false,
"project": "pr_…",
"new_migrations": ["0011_add_refunds.sql"],
"branch_upload": "up_…",
"base_upload": "up_…",
"base_commit": "…"
}
| Command | Prints on success | Exit |
|---|---|---|
karts up --output url | the URL and a newline, once the environment is ready | 0; else 1, reason on stderr |
karts url … --output json | schema_version, env, name, url, state, key | 0; 1 when none serves |
karts list --output json | every environment of the project | 0 |
karts down … --output json | state: "gone" | 0 |
createdis true when this call created the environment, so its URL is new. The name, and so the URL, stays the same for every laterupof the same branch and workspace until the environment is taken down or reaped.- A failure prints
{"schema_version": 1, "error": {"code", "message", "details"}}on stdout and the message on stderr, and exits 1. Usage errors exit 2 and print no JSON. - Fields are only ever added within a schema version. Pin the CLI version in CI.
- For CI, issue a CI token on the Karts dashboard and set it as
KARTS_TOKEN. A project-scoped token works in that one project only. Tokens last 90 days unless you choose otherwise.
4. One backend per pull request
The workflow we are testing keeps one Karts backend per open pull request, and nothing for closed ones:
- On every pull-request event, and hourly, it asks GitHub what the pull request is now. Open, from this repository, into the default branch:
karts upfor its current head and set the preview'sKARTS_BACKEND_URL. Anything else:karts downand remove the variable. - It runs only the default branch's copy of the workflow, and treats the pull request's code as data: nothing from the pull request runs on the CI runner, only inside the Karts VM and on Vercel's own builders. Pull requests from forks are not supported.
- Karts' idle reaper is the last backstop for anything left behind.