Configuration

Your app setup mainly lives in one small file. Name the app, list the Cloudflare resources it uses and Goribu generates the rest.

updated in 0.9.4
On this page

goribu.config.js

goribu.config.js tells Goribu what to deploy: the Worker's name, its address and any database, key-value store, or file storage it needs. Goribu turns it into Cloudflare's own Wrangler configuration (Workers configuration), so you don't write that by hand. A minimal app needs only a name and a date:

goribu.config.js
import { defineConfig } from "goribu/server";

export default defineConfig({
  name: "my-app",
  compatibilityDate: "2026-09-01",
});
  • Plain JavaScript: Node loads this file directly, even in TypeScript apps. Don't import JSX, components or Vite aliases here.
  • One place to configure: Goribu generates goribu.wrangler.jsonc from this file on every run. Edit goribu.config.js, never the generated file.
  • Changes in development: Saving the file restarts pnpm dev. If the new config is invalid, the error is shown and the running server keeps the old one until you fix it.

Options

OptionWhat it doesDefault
nameThe Worker's name on Cloudflare. Deploying with an existing name updates that Worker.Required
compatibilityDateWhich Workers runtime behavior your app expects, as YYYY-MM-DD. Use today's date for a new app.Required
compatibilityFlagsExtra Workers compatibility flags to turn on.None
domainYour app's own hostname, such as app.example.com. See custom domain.Only the workers.dev address
workersDevSet false to turn off the workers.dev address. See workers.dev address.true
databaseA D1 database, or your own Postgres database.None
kvA KV namespace.None
r2R2 buckets for files and uploads.None
responseHeadersDefault headers for responses from your Worker. See response headers.None
wranglerCloudflare features Goribu doesn't manage, such as Workers AI or service bindings, written as Wrangler fields. See Wrangler bindings.None

Resources

Declare a resource by name and Goribu does the rest: deploy creates it if it's missing, connects it to your Worker, and your handlers use it as req.d1, req.kv, or req.r2. You never copy resource IDs around:

goribu.config.js
import { defineConfig } from "goribu/server";

export default defineConfig({
  name: "my-app",
  compatibilityDate: "2026-09-01",
  database: { type: "d1", name: "my-app-db" },
  kv: { name: "my-app-kv" },
  r2: { name: "my-app-files" },
});
  • One of each: An app has at most one database (D1 or Postgres), one KV namespace and the two R2 buckets described on the R2 page.
  • Names choose resources: Changing a name points the app at a different resource; the old one isn't renamed or deleted. Removing a declaration also leaves the resource and its data in place.
  • Development: pnpm dev uses local copies of each resource, so you never touch production data while developing.
  • Raw bindings: The underlying Cloudflare bindings are req.env.DB, req.env.KV, and req.env.R2, for libraries that expect them.
  • Other Cloudflare features: Workers AI, service bindings, Vectorize and similar go in a wrangler block. Goribu passes them through without creating anything.
  • Postgres is yours: Goribu doesn't create a Postgres database. You bring one, and deploy sets up the Cloudflare connection to it. See Postgres.
  • Extra settings: D1 also accepts replication (D1) and R2 an option to locate both buckets in the EU. Add jurisdiction: "eu" to r2 to do that, but decide before storing files, as changing it later points the app at new, empty buckets.

Custom domain

By default your app is live at a workers.dev address. To serve it from your own hostname, add domain and deploy connects it for you:

goribu.config.js
import { defineConfig } from "goribu/server";

export default defineConfig({
  name: "my-app",
  compatibilityDate: "2026-09-01",
  domain: "app.example.com",
});
  • What you need: The domain must be an active zone in the Cloudflare account you deploy with. Use either the domain itself (example.com) or a subdomain (app.example.com).
  • What deploy does: Cloudflare creates the DNS record and TLS certificate automatically. The address can take a few minutes to start working after the first deploy.
  • Preview first: pnpm check shows whether the domain can be connected, without changing anything.
  • www and redirects: Goribu connects exactly one hostname. To set up www, redirects, and extra hostnames in the Cloudflare dashboard.
  • Changing it later: Changing or removing domain doesn't disconnect the old hostname. Remove it in the Worker's Settings > Domains & Routes.
  • Files domain: If you also serve public files from R2, use a different hostname for r2.domain, such as media.example.com.

workers.dev address

As mentioned above, your app gets https://<name>.<your-subdomain>.workers.dev. Once your own domain works, a second address can cause problems: sign-in callbacks, cookies and allowed-origin rules usually expect one host. Turn it off with workersDev: false:

goribu.config.js
import { defineConfig } from "goribu/server";

export default defineConfig({
  name: "my-app",
  compatibilityDate: "2026-09-01",
  domain: "app.example.com",
  workersDev: false,
});
  • Wait for your domain: Keep workers.dev on until your domain is connected and loading. It's the address that works right away.
  • Needs a domain: workersDev: false requires domain, so the app always has an address.
  • Config wins: Every deploy applies this setting. To turn workers.dev back on, remove the field rather than changing it in the dashboard.
  • Preview URLs: Deploys always turn off Cloudflare's per-version preview URLs. Goribu deploys straight to production, so they would only be extra public copies of your app.

Environment variables

Use environment variables for API keys and settings that differ between your machine and production, such as a payment provider's test and live keys. Most apps need two files in the app root:

  • .env: Values for local development.
  • .env.production: Values for production. Deploy uploads them to Cloudflare as Worker secrets.
.env
CATALOG_API_TOKEN=local-test-token
PUBLIC_API_ORIGIN=http://localhost:4000
.env.production
CATALOG_API_TOKEN=live-token
PUBLIC_API_ORIGIN=https://api.example.com

Keep both files out of Git. If you work with a team, you can commit a .env.example with placeholder values instead, so others know which keys the app needs.

Reading values

Server code reads every value from req.env, both locally and in production:

Server code
import type { Handler } from "goribu/server";

type Env = { CATALOG_API_TOKEN: string };

export const GET: Handler<Record<string, unknown>, Env> = async (req, res) => {
  const response = await fetch("https://catalog.example.com/items", {
    headers: { Authorization: `Bearer ${req.env.CATALOG_API_TOKEN}` },
  });
  if (!response.ok) {
    return res.status(502).json({ error: "Catalog unavailable." });
  }

  return res.json(await response.json());
};
Server code
export const GET = async (req, res) => {
  const response = await fetch("https://catalog.example.com/items", {
    headers: { Authorization: `Bearer ${req.env.CATALOG_API_TOKEN}` },
  });
  if (!response.ok) {
    return res.status(502).json({ error: "Catalog unavailable." });
  }

  return res.json(await response.json());
};

Browser code only sees names that start with PUBLIC_, through import.meta.env:

Browser code
// Anyone can read this value in the page's JavaScript.
const apiOrigin = import.meta.env.PUBLIC_API_ORIGIN;
Browser code
// Anyone can read this value in the page's JavaScript.
const apiOrigin = import.meta.env.PUBLIC_API_ORIGIN;
  • Never prefix secrets: PUBLIC_ values are copied into your JavaScript at build time, where anyone can read them. Never give an API key that prefix.
  • Rebuild to change them: Because they're copied at build time, changing a PUBLIC_ value means building and deploying again.

Production secrets

Deploy checks .env.production, then uploads each key as a Worker secret.

  • Without the file: Deploy keeps the secrets already on the Worker. It never uploads your local .env. So you are free to add the secrets directly in the Cloudflare dashboard and avoid the .env.production file.
  • Removing a key: Deleting a line doesn't delete the secret on Cloudflare. Secrets stay as they were and have to be removed manually.
  • Preview: pnpm check says whether deploy will upload the file or keep existing secrets. It never prints values.

Overrides and other files

The two files above cover most apps. These come up when you need more:

  • .env.local: Overrides .env on your machine, such as your own API key in a team that shares a .env. Keep it out of Git.
  • Shell variables: A PUBLIC_ value already set in your shell, such as in CI, wins over the files.
  • PUBLIC_ values in production: A production build reads PUBLIC_ values from .env too, unless .env.production sets them. Set every PUBLIC_ key in .env.production so local values don't end up in the live app.
  • .env.development: Only affects PUBLIC_ values in development. Server values in it never reach req.env. Put them in .env or .env.local.
  • .dev.vars: Cloudflare's own local file. If it exists, even empty, the server ignores .env and .env.local, so use one or the other.
  • goribu.config.js: Doesn't load any of these files. If it needs to read a process.env, set the variable in your shell.

Development-only code

import.meta.env.DEV, PROD, and MODE also work in server code. Production builds remove code that only runs when DEV is true, which makes them safe for shortcuts such as a local sign-in:

src/routes/dev-login.ts
import type { Handler } from "goribu/server";

export const POST: Handler = async (req, res) => {
  if (!import.meta.env.DEV) {
    return res.notFound();
  }

  req.cookies.set("session", await createDevSession(req), {
    path: "/",
    httpOnly: true,
  });
  return res.redirect("/");
};
src/routes/dev-login.js
export const POST = async (req, res) => {
  if (!import.meta.env.DEV) {
    return res.notFound();
  }

  req.cookies.set("session", await createDevSession(req), {
    path: "/",
    httpOnly: true,
  });
  return res.redirect("/");
};

Response headers

Use responseHeaders to add the same headers to every response from your Worker, such as security headers:

goribu.config.js
import { defineConfig } from "goribu/server";

export default defineConfig({
  name: "my-app",
  compatibilityDate: "2026-09-01",
  responseHeaders({ request, env, mode, nonce }) {
    return {
      "X-Content-Type-Options": "nosniff",
      "Referrer-Policy": "strict-origin-when-cross-origin",
    };
  },
});
  • Arguments: The function receives the request, env, mode (development or production), and the request's nonce for a Content Security Policy. Return an object of header values; it can't be async.
  • Your routes win: Headers set by a route or by Goribu override these defaults.
  • Prerendered pages don't get them: Static files and prerendered pages are served without running the Worker, so they don't receive these headers. Use a handler or prerender = false for pages that need them.
  • No default security policy: Goribu doesn't set a Content Security Policy or CORS headers for you. See Allow cross-origin requests for CORS.
  • Not for everything: Headers such as Content-Type, Location, and Set-Cookie belong to the response itself and can't be set here.

Vite settings

Build tooling such as Tailwind, import aliases, and other Vite plugins goes in vite.config.js, not goribu.config.js. Keep goribu() in the plugins list.

To import src/components/button as @/components/button, add an alias with Vite's resolve.alias:

vite.config.js
import { fileURLToPath } from "node:url";
import { goribu } from "goribu/vite";

export default {
  plugins: [goribu()],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
};
  • Where aliases work: In application code during development and builds. Not in goribu.config.js, which Node loads without Vite.