Configuration

silgi.config.ts in the app root default-exports the config, wrapped in defineConfig so the editor checks the keys. The silgi reference lists every key with its type; this page covers the two that carry your own values, runtimeConfig and appConfig, how environment variables and .env files reach them, and the keys for hosts, link protocols, teleports, prerender conflicts, the Cloudflare environment and the cache deadline.

Runtime config

runtimeConfig holds values that can change per deployment. Keys at the top level are private: they stay on the server. Keys under public are sent to the browser.

import { defineConfig } from "silgi";

export default defineConfig({
  framework: "react",
  runtimeConfig: {
    apiSecret: "",
    oauth2ClientId: "",
    limits: { perMinute: 60 },
    public: { siteName: "My shop", apiBase: "/api" },
  },
  appConfig: { theme: "light" },
});

Read it on the server with useRuntimeConfig(event) from silgi/server, or without the event (also at module scope, and as server.runtimeConfig in a server plugin). It returns the whole object, private keys and public. A load gets the client part as ctx.runtimeConfig.

import { defineHandler, useRuntimeConfig } from "silgi/server";

export const site = defineHandler({
  route: "/api/site",
  method: "GET",
  handler(_, { event }) {
    const config = useRuntimeConfig(event);
    return { signed: typeof config.apiSecret === "string" && config.apiSecret !== "", site: (config.public as { siteName?: string }).siteName };
  },
});

In app code (a page, a layout, a component, a plugin) call useRuntimeConfig() from the adapter, with no event. It returns { public, app } on the server and in the browser: public is the public part, and app is { baseURL, buildAssetsDir }. The private keys are never in it.

import { useAppConfig, useRuntimeConfig } from "@silgi/react";

export default function Header() {
  const { public: site, app } = useRuntimeConfig();
  const settings = useAppConfig();
  return (
    <header data-theme={String(settings.theme)}>
      <a href={app.baseURL}>{String(site.siteName)}</a>
    </header>
  );
}

process.env is not the way to read a value in client code. silgi build and silgi generate fail when a client module reads process.env.<NAME> other than NODE_ENV, with app/components/banner.tsx:2:13: process.env.SITE_NAME reaches the client bundle; use useRuntimeConfig().public, import.meta.env or #silgi/env/public, and silgi dev shows the same text as a warning. Code under server/ is not checked, and neither is a read behind import.meta.server or a typeof process test. Put the value in runtimeConfig.public and read it with useRuntimeConfig():

export default function Banner() {
  return <p>{process.env.SITE_NAME}</p>;
}
import { useRuntimeConfig } from "@silgi/react";

export default function Banner() {
  return <p>{String(useRuntimeConfig().public.siteName)}</p>;
}

The server writes the public part into the page once, and the browser starts with the same values the server rendered. After silgi typegen, the keys of runtimeConfig.public are typed on useRuntimeConfig().public. The object that useRuntimeConfig(event) returns is shared by every request: do not change it.

Environment variables

A variable overrides a key of the config when the server starts:

  • SILGI_<KEY> overrides the top-level key <KEY>, and SILGI_PUBLIC_<KEY> overrides the key under public.
  • The key is written in upper snake case: apiBase is API_BASE, and oauth2ClientId is OAUTH2_CLIENT_ID.
  • Only keys the config declares are overridden. A variable for a key that is not in runtimeConfig does nothing, and so does SILGI_PUBLIC alone.
  • Only top-level keys are matched. To change a key that holds an object, set the whole object as JSON.
  • A value that is valid JSON is parsed (120 is a number, true a boolean, {"perMinute":120} an object); any other value stays a string.
  • The variables are read once, at server start. Changing one needs a restart.
SILGI_API_SECRET=s3cret
SILGI_OAUTH2_CLIENT_ID=abc123
SILGI_LIMITS={"perMinute":120}
SILGI_PUBLIC_SITE_NAME=Corner shop

With this file and the config above, useRuntimeConfig(event).limits is { perMinute: 120 } and useRuntimeConfig().public.siteName is Corner shop.

For values that need a schema, a type and a check at start, declare them in app/env.ts; see Environment. Modes (--mode staging) and the $production and $env config blocks are there too.

.env files

silgi dev, build, generate and preview read .env, .env.<mode> and .env.local from the app root, and --dotenv <file> adds another file; Getting started has the details. A variable already set in the shell wins over the files.

A built server does not read these files. Bun loads a .env from the working directory when it starts a process, and Node loads none, so in production set real environment variables on the host.

App config

appConfig in the config, merged with the default export of app/app.config.ts, is the app config. The file wins on a conflict. useAppConfig() returns it in every library, and defineAppConfig from silgi/app types the file. It is bundled into the browser build and sent with the page, so it is public: never put a secret in it. It is not changed by environment variables.

import { defineConfig } from "silgi";

export default defineConfig({
  framework: "react",
  devServer: { allowedHosts: [".ngrok.app", "app.test"] },
  server: { trustProxy: true, allowedHosts: ["example.com", ".example.com"] },
  app: {
    linkProtocols: ["http", "https", "mailto", "tel", "sms", "whatsapp"],
    teleportId: "teleports",
    teleportTag: "div",
    teleportAttrs: { class: "teleports" },
  },
  prerender: { conflict: "error" },
  cloudflare: { env: "staging" },
});
KeyDefaultMeaning
devServer.allowedHosts[]extra hosts that silgi dev and silgi preview answer. An entry is an exact name, or starts with . for the name and every subdomain (.ngrok.app admits a.ngrok.app); true allows every host. localhost, *.localhost, IP addresses and the --host or devServer.host value are always allowed. Any other host gets a 403 that names it. A built production server answers any host
devServer.prefetchfalsein silgi dev, Link with prefetchOn="visibility" prefetches nothing, and hover and focus prefetch load the page chunks only and run no load, since each load would run on a server that recompiles. true keeps the data prefetch of a build on hover and focus. A built app is not affected
server.allowedHoststruethe hosts whose x-forwarded-host header getRequestURL trusts when server.trustProxy is on. Entries match as in devServer.allowedHosts. true trusts any host; without trustProxy the key does nothing
app.linkProtocols["http", "https", "mailto", "tel", "sms"]the schemes that a Link, navigate(), navigateTo(), redirect() or sendRedirect target may carry, written without : and in lower case. A path, ?query or #hash target has no scheme and is not checked. See Routing
app.cookieStoretruethe browser jar follows the cookieStore change event where the browser has it, so a cookie another tab wrote reaches useCookie without a call to refreshCookie
app.data.staleTime0milliseconds the browser keeps the data of a load fresh; meta.staleTime of a page overrides it. See Data
app.data.gcTime300000milliseconds after a page stops showing its data before the browser deletes it; meta.gcTime overrides it
app.data.staleReload"blocking""background" shows stale data at once and runs the load after the navigation
app.data.refreshOnFocusfalsea number of milliseconds: refresh() when the tab becomes visible, at most once in that time
app.data.refreshOnReconnectfalsetrue calls refresh() when the browser goes online
experimental.offlineRetryfalsea navigation, prefetch or useFetch that failed on the network waits for online and tries again
app.teleportId"teleports"id of the element that the document writes after the app root for teleports rendered on the server (Vue <Teleport to="#teleports">, Solid Portal)
app.teleportTag"div"tag of that element
app.teleportAttrs{}more attributes of that element, written after id and escaped
app.asyncData.pendingWhenIdlefalsetrue makes pending of useAsyncData and useFetch also true for an idle entry that has no data yet, for example one started with immediate: false
prerender.conflict"warn"what the prerender does when two routes give one path, for example blog/new.tsx and blog/[slug].tsx with /blog/new in prerender.routes: "warn" logs it and writes the route the router picks, "error" fails the build, "ignore" says nothing
cloudflare.envunsetthe wrangler environment whose env.<name> block the cloudflare build copies into .output/wrangler.json
routeRules[...].cache.maxResolveTime30seconds a cache fill may run before the callers waiting for it get a 504; 0 or Infinity waits without a limit. See Storage and cache

A value of the wrong type fails when the config loads, with a message that names the key, for example app.teleportId must be a non-empty string (got "").

import { defineConfig } from "silgi";

export default defineConfig({
  framework: "react",
  routeRules: {
    "/prices": { cache: { maxAge: 60, maxResolveTime: 10 } },
    "/report": { cache: { maxAge: 3600, maxResolveTime: 0 } },
  },
});