Docs
Build your application (static files, a fetch app or Next.js) and deploy the folder with one command, from the dashboard, or from GitHub. Every deploy is a snapshot you can roll back to.
1. Deploy
Create an application in the dashboard (its name is also its address), then deploy from the folder your build wrote. The first command opens your browser to sign in; the CLI keeps a token for next time.
npx @swarza/cli login npx @swarza/cli deploy --app my-app --prod
deployuploads.swarza/when it exists (the Next.js adapter's output), otherwise the current folder; name another one withswarza deploy ./out. It leaves out.gitand what.swarzaignorelists, and follows symlinks.- The application comes from
--app,SWARZA_APPor"app"inswarza.json; the first deploy asks and saves it there. - It waits until the new version is live everywhere and prints its address. Only files that changed since the last deploy are stored again.
- Other commands:
swarza whoami,swarza apps,swarza apps create <name>,swarza previews,swarza previews rm <name>,swarza logout.
2. Previews
Without --prod, swarza deploy makes a preview named after your git branch (or --preview <name>) at https://my-app--<name>.sites.stg.swarza.com. Deploying the same name again updates it. Production doesn't change.
- Previews share the application's environment variables, databases and buckets, so test changes to data with care. Scheduled jobs run only in production.
- Search engines are asked not to index previews. “Make live” on the Deployments tab puts a preview's files in production.
- Up to 2, 10 or 20 previews per application by plan; one nobody updates for 7 or 30 days (by plan) is removed.
3. Dashboard upload
On the application's Deploy tab, drop your build folder or a .zip (or choose one), as production or as a named preview. The browser zips a folder before it uploads it.
4. GitHub Actions
Pushes to your main branch go live; every pull request gets the preview pr-<number>, a comment with its address and a GitHub deployment, and closing it removes the preview. Create a token under Tokens in the dashboard (limit it to the application) and add it to the repository as the SWARZA_TOKEN secret.
# .github/workflows/deploy.yml on: push: { branches: [main] } pull_request: types: [opened, synchronize, reopened, closed] jobs: deploy: runs-on: ubuntu-latest permissions: { contents: read, deployments: write, pull-requests: write } steps: - uses: actions/checkout@v4 - run: npm ci && npm run build if: github.event.action != 'closed' - uses: swarza/deploy-action@v1 with: token: ${{ secrets.SWARZA_TOKEN }} app: my-app
Anywhere else, run the CLI with the token: SWARZA_TOKEN=… npx @swarza/cli deploy --prod.
5. Folder layout
out/ index.html, assets/… # static files (or put them under publicDir) server.mjs # optional: a fetch app (see below) swarza.json # optional settings
swarza.json and .swarza/ are never served as files. Deploy the one folder that holds everything: each deploy replaces the whole application.
6. swarza.json
{ "publicDir": "", "spa": true, "redirects": [{ "from": "/old/*", "to": "/new/:splat", "status": 301 }], "headers": [{ "source": "/*", "headers": { "X-Frame-Options": "DENY" } }] }
Paths resolve in this order: redirects, the exact file, /index.html, .html (clean URLs), the SPA fallback, then your 404.html. Files with a hash in the name are cached for a year; HTML revalidates every time.
7. Fetch apps (Node.js and Bun)
Export a fetch handler, and swarza runs it on its own servers in real Node.js (the default) or Bun: node:* modules, node_modules, native packages and Bun.* APIs all work. Nothing runs while nobody visits, and a request is answered in milliseconds. Upload the application with its node_modules and a runtime section:
// index.mjs export default { async fetch(request, env, ctx) { return Response.json({ hello: new URL(request.url).pathname }); }, }; // swarza.json { "runtime": { "engine": "node", "entry": "index.mjs" } }
engineisnode(Node.js 24) orbun.entryis a path in your upload (.mjs,.js,.cjsor, with type stripping in Node.js and natively in Bun,.ts).- The entry exports
{ fetch }, an app with afetchmethod (Hono, Elysia) or a function.request.urlis the visitor'shttps://URL; their address is inX-Forwarded-For.ctx.waitUntil()accepts work that finishes after the response. - Your files are read-only; write temporary files to
/tmp(64 MB). Outbound requests to the internet work; listening on ports doesn't. - A worker stays warm for 60 seconds after the last request, is replaced after 10,000 requests, and is stopped if a request gets no answer within 30 seconds (504). Keep state in a database, not in memory.
- Memory is 256 MB, 512 MB or 1 GB by plan, and warm time counts against the plan's app compute allowance. Every request goes to your handler; set
Cache-Controlon responses you want cached at the edge. - Environment variables set in the dashboard (the application's Environment tab) are in
process.envand inenv, together with your databases. Changing one restarts the application within seconds.
8. Next.js
Next.js 16.2 and later deploy with the swarza adapter: routing, middleware (Node.js and edge), Server Actions, streaming, ISR, on-demand revalidation and next/image. Files no middleware or rule touches are served without starting your application.
npm install --save-dev @swarza/next // next.config.mjs import { createRequire } from "node:module"; const require = createRequire(import.meta.url); export default { adapterPath: require.resolve("@swarza/next") }; // build and deploy next build npx @swarza/cli deploy --app my-app --prod
- The build runs Vercel's official Next.js adapter, which writes the standard Build Output format, and packs it into
.swarza/. Monorepos and pnpm work. Any other framework that writes.vercel/outputdeploys withnpx swarza-build-output. - Your application starts on the first request in well under a second and sleeps after a minute without requests. Prerendered and regenerated pages and
revalidatePath/revalidateTagare kept in your application's store, so they survive restarts and show at once. - Native modules other than sharp need a build on Linux arm64.
output: "export"is an application with only static files: upload theoutfolder.
9. Databases
SQLite-compatible databases (the Turso engine) on the same servers as your applications: a query takes well under a millisecond, with no network in between. Create one under Databases in the dashboard and bind it to an application; fetch apps and Next.js applications get DATABASE_URL and DATABASE_AUTH_TOKEN. They speak libSQL: use @libsql/client, or Drizzle and other tools built on it.
import { createClient } from "@libsql/client/web"; import { drizzle } from "drizzle-orm/libsql/web"; const db = drizzle(createClient({ url: process.env.DATABASE_URL, authToken: process.env.DATABASE_AUTH_TOKEN, }));
Migrations and tools reach the database from anywhere with a token from its page (read-write or read-only, shown once):
DATABASE_URL=libsql://db.sites.stg.swarza.com \ DATABASE_AUTH_TOKEN=<token> \ npx drizzle-kit migrate # dialect: "turso" in drizzle.config.ts
- As many databases as you need, on every plan, holding up to 10 GB together (all of your account's databases). At the limit, writes that need more space fail; reading, updating and deleting still work, and deleted rows make room again. One database can serve several applications (each binding has its own name,
<NAME>_URLand<NAME>_AUTH_TOKEN, and can be read-only), and all of an application's workers share it. - Download a database as a SQLite file with any of its tokens:
curl -H "Authorization: Bearer <token>" https://db.sites.stg.swarza.com/download -o my.db. - Every write is backed up within about a second. Restore to any moment of the last 30 days from the database's page; the restore replaces the database, and the state before it stays in the backups. Deleted databases are kept 30 days.
- Writes are queued and applied one at a time, in about 30 microseconds each, so keep transactions short; reads run in parallel. A crash of the server can lose at most the last 0.2 seconds of writes.
- Use
@libsql/client/web(plain JavaScript) in applications you upload: the default entry loads a native module that would need a build on Linux arm64.
10. Storage
Buckets for your applications' files: uploads, avatars, documents. They speak S3, so the AWS SDK, the AWS CLI, rclone and Cyberduck work with them. Create one under Storage in the dashboard and bind it to an application. Every bucket has its own address, https://<bucket>.s3.stg.swarza.com: S3 clients connect there, and a file is at https://<bucket>.s3.stg.swarza.com/<key>. The application's fetch apps and Next.js applications get STORAGE_ENDPOINT, STORAGE_BUCKET, STORAGE_REGION, STORAGE_ACCESS_KEY_ID and STORAGE_SECRET_ACCESS_KEY.
import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; const s3 = new S3Client({ endpoint: process.env.STORAGE_ENDPOINT, region: process.env.STORAGE_REGION, credentials: { accessKeyId: process.env.STORAGE_ACCESS_KEY_ID, secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY, }, }); await s3.send(new PutObjectCommand({ Bucket: process.env.STORAGE_BUCKET, Key: "avatars/1.png", Body: file })); const url = await getSignedUrl(s3, new GetObjectCommand({ Bucket: process.env.STORAGE_BUCKET, Key: "avatars/1.png" }), { expiresIn: 600 });
- Public buckets let anyone read a file at its address, as S3 does (bound applications get the bucket's address as
STORAGE_PUBLIC_URL), through the CDN. The CDN caches a file for as long as itsCache-Controlsays, so upload with one (for names that never change,public, max-age=31536000, immutable); without it, every request reads the file from the bucket. Private buckets answerAccessDeniedwithout a key: they are read with a key or with signed links. Switch between them on the bucket's page. - Your own domain, such as
files.example.com: add it on the bucket's page and point a CNAME at the address shown there; the certificate follows in a few minutes. To sign links for it, give the SDK that domain as the bucket's endpoint:new S3Client({ endpoint: "https://files.example.com", bucketEndpoint: true, ... })withBucket: "https://files.example.com". - From your machine or CI, use a key from the bucket's page:
aws s3 cp photo.jpg s3://<bucket>/photos/ --endpoint-url https://s3.stg.swarza.com. - Every S3 request counts toward your plan's requests, and downloads toward its transfer, as for your applications. Buckets share your plan's storage with your applications' files; files can be as large as your plan allows. Buckets are counted from their uploads within seconds: once your storage is full, they stop taking uploads within seconds (deleting still works), and a file larger than your plan allows is removed after its upload. A deleted file is gone at once; a deleted bucket takes its files and keys with it.
- A bucket's name is also an address, so bucket and application names are one list: a name used by one can't be used by the other.
11. Scheduled jobs
Run your own code on a schedule: export a function from a module in your application, and list it with a five-field cron schedule (in UTC) in swarza.json. On each schedule swarza calls the function in its own sandboxed worker, with your app's environment variables and databases. It has no URL, so only swarza can start it. The function gets the same arguments as a Cloudflare Workers scheduled handler.
// jobs/cleanup.mjs export default async function (controller, env, ctx) { console.log("cleaning up", controller.cron, new Date(controller.scheduledTime)); ctx.waitUntil(sendReport()); // awaited before the run ends } // jobs/tasks.mjs export async function digest(controller, env) { /* ... */ } // swarza.json { "runtime": { "entry": "index.mjs" }, "scheduledJobs": [ { "schedule": "0 3 * * *", "function": "jobs/cleanup.mjs" }, { "schedule": "0 8 * * 1-5", "function": "jobs/tasks.mjs#digest" } ] }
- Fetch apps:
functionis a module in your upload (.mjs,.jsor.ts), with#namefor a named export; without it, the default export (a function, or an object withscheduled). Next.js: putswarza.jsonnext tonext.config.mjswith paths in your project ("jobs/cleanup.ts"); the adapter bundles each job with what it imports. - Hobby: 5 jobs, at most every 15 minutes, 1 minute per run. Starter: 50, every minute, 5 minutes. Pro: 100, every minute, 15 minutes. A run over its time is stopped. A deploy that asks for more jobs, runs them more often, or names a module that isn't in the upload fails with a clear message.
- A run starts within its scheduled minute (not at second 0). A run due while the previous one is still going starts right after it; a job never runs twice at once, and a busy server makes a run wait, not skip. Runs count against your app compute allowance (memory × time); while your account is over its allowances, or the application is capped, jobs pause.
- The application's Scheduled jobs tab shows the next and last runs, with Run now. What the function prints and each run's result are in its Logs tab. Set either the day of the month or the day of the week, not both.
12. Deploys
- A deploy is one upload of the whole folder: it goes live when it is built, usually in seconds, and the edge serves it everywhere within about a minute. Visitors see the old version or the new one, never a mix.
- The Deployments tab lists every deploy with where it came from (CLI, GitHub, dashboard) and its git commit. Roll back to any kept deployment with one click.
- Kept: the last 5 deployments or 7 days on Hobby, 20 or 30 days on Starter and Pro, plus the live one and every preview's.
13. Limits
Allowances are shared by all applications in your account; you can cap a single application in its Settings. Past 100% applications slow down instead of costing you more. See pricing.