Krantho is in active development and not yet published to npm. Read what that means today.
esc

Search the documentation and product pages. Type “theme” to switch themes.

Configuration

The krantho configuration tree — how defaults, krantho.config.ts and the environment resolve into one validated object, and every setting with its variable.

IntermediateApplies toNext.jsSvelteKitExpress / Vite

Everything a krantho site can be told is a leaf of one typed tree. You write the leaves you care about in krantho.config.ts. A deployment overrides any of them from the environment. Everything else keeps its shipped default.

krantho.config.ts
import { defineKranthoConfig } from '@krantho/config'

export default defineKranthoConfig({
  site: {
    domain: 'example.com',
    name: 'My Site',
    language: 'en',
    baseUrl: 'https://example.com',
  },
  auth: { mode: 'token' },        // the secret comes from KRANTHO_API_TOKEN
  llm: { provider: 'anthropic' }, // the key comes from KRANTHO_LLM_ANTHROPIC_API_KEY
  seo: { configDir: './krantho.seo' },
})

The object defineKranthoConfig() returns is what you hand to krantho where it is wired into your app, next to your collections:

app/api/[...path]/route.ts
import { createApiRouteHandlers } from '@krantho/server-next'
import config from '../../../krantho.config'
import { collections } from '../../../krantho.collections'

// The content API at /api/*: content, media, SEO, auth and the sitemap
export const { GET, POST, PUT, DELETE, PATCH } = createApiRouteHandlers({
  config,
  collections,
})

// The admin at /admin, in app/admin/[[...slug]]/route.ts:
//
//   import { createAdminRouteHandler } from '@krantho/server-next'
//   export const { GET } = createAdminRouteHandler()
src/routes/api/[...path]/+server.ts
import { createApiHandlers } from '@krantho/server-sveltekit'
import config from '../../../../krantho.config'
import { collections } from '../../../../krantho.collections'

// The content API at /api/*: content, media, SEO, auth and the sitemap
export const { GET, POST, PUT, DELETE, PATCH } = createApiHandlers({
  config,
  collections,
  // Editors preview drafts through a short-lived signed link
  sitePreview: 'signed-url',
})

// The admin at /admin, in src/routes/admin/[...path]/+server.ts:
//
//   import { createAdminHandler } from '@krantho/server-sveltekit'
//   export const { GET } = createAdminHandler()
server.ts
import express from 'express'
import { createAdminServer, createHandler, toWebRequest, writeResponseHeaders } from '@krantho/server-node'
import config from './krantho.config'
import { collections } from './krantho.collections'

const api = createHandler({ config, collections, sitePreview: 'signed-url' })
const admin = createAdminServer(config)
const app = express()

async function send(res: express.Response, response: Response): Promise<void> {
  res.status(response.status)
  writeResponseHeaders(res, response)
  res.end(Buffer.from(await response.arrayBuffer()))
}

const owns = (prefix: string, path: string) => path === prefix || path.startsWith(`${prefix}/`)

// Mounted without a path: the handler routes on the full URL
app.use(async (req, res, next) => {
  if (owns('/api', req.path)) return send(res, await api.handle(await toWebRequest(req)))
  if (!owns('/admin', req.path)) return next()
  const url = new URL(`http://localhost${req.originalUrl}`)
  return send(res, admin({ url, cookie: req.headers.cookie }))
})

How a value is resolved#

One rule, applied once at startup:

defaults → krantho.config.ts → the environment.

The environment wins. Relative filesystem paths resolve against the working directory. Every problem found — a value that will not parse, a required field nobody set, a rule spanning two leaves — is collected and thrown as a single error naming each variable and path. A misconfigured deployment fails at boot with the whole list, not one item at a time:

text
Invalid krantho configuration:
  - KRANTHO_MEDIA_MAX_SIZE (media.maxFileSize): expected an integer, got "abc"
  - site.baseUrl: must be an absolute URL

Configuration versus wiring#

The split is what makes environment overrides mechanical.

Configuration is data — strings, numbers, booleans, lists, URLs. It lives in krantho.config.ts, and every scalar or list leaf has an environment variable. Structured leaves — the SEO archetype maps, the image presets and the webhooks — hold nested data and are set in code only.

Wiring is code that cannot be a string: collections from defineCollection(), page definitions, plugins, lifecycle hooks, a content change listener and how editors preview drafts (sitePreview). It stays an argument to the handler.

app/api/[...path]/route.ts
import { createApiRouteHandlers } from '@krantho/server-next'
import config from '../../../krantho.config'
import { collections } from '../../../krantho.collections'

export const { GET, POST, PUT, DELETE, PATCH } = createApiRouteHandlers({
  config,                                  // data
  collections,                             // wiring
  plugins: [],                             // wiring
  hooks: {},                               // wiring
  onContentChange: (change) => {},         // told about every committed write
})

Where media files are stored and how pages are cached are configuration as well: media.provider picks the file store, cache.pages the page cache, and their settings are leaves like any other.

The sections#

SectionWhat it holds
siteDomain, name, language, base URL, supported and default locale. The only section that must be supplied.
pathsData directory, content directory, and the directory the SEO pipeline writes generated files to.
persistenceBackend choice plus the SQLite, Postgres and filesystem settings. See Persistence.
contentThe content cache of the API and of server-rendered pages, at-notation, the file watcher, default statuses, list and search limits, version retention, lock lease.
authMode, token identity, JWT secret, OIDC endpoints, signup policy and how /admin treats a visitor without a session (adminEntry), session and cookie settings. See The admin dashboard.
mediaThe file store (provider: local, vercel-blob, s3 or gcs) and its settings, plus size cap, accepted MIME types, and the image pipeline. With provider unset, media is off. See Media.
llmProvider, model and base URL for enrichment, one credential for each provider, and per-model request settings (profiles).
secretsThe vault that encrypts the secrets a site stores in its database, such as a key pasted in Studio, and the key it uses.
assistantWhether the Studio assistant is on, the provider and model it uses, who may use it and its limits. See The Studio assistant.
seoSite identity type, archetypes per collection and route, rich per-scope configuration, seed mode, seed directory, enrichment limits.
cacheHow pages are cached (none, next-isr, cdn), the CDN in front of the site and its settings, and whether several Next.js instances share one cache in the database (shared). See Rendering and caching and Deploy behind a CDN.
webhooksOutgoing webhooks: a list of { url, events, secret? }. See Webhooks.
toolsConcurrency and content caps for the build-time pipeline.

Every setting below lists each leaf with its environment variable, its type, its default and what it does.

Environment variables#

These leaves use established names instead of the mechanical form: KRANTHO_AUTH, KRANTHO_API_TOKEN, KRANTHO_JWT_SECRET, KRANTHO_TOKEN_ROLE, KRANTHO_TOKEN_EMAIL, KRANTHO_ADMIN_EMAILS, KRANTHO_SIGNUP, OIDC_ISSUER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI, OIDC_POST_LOGOUT_URI, KRANTHO_MEDIA_MAX_SIZE, KRANTHO_IMAGE_CACHE_MAX_ENTRIES, KRANTHO_IMAGE_CACHE_MAX_MB, KRANTHO_IMAGE_CACHE_TTI_SECS, KRANTHO_LLM_CLAUDE_CODE_OAUTH_TOKEN (llm.claudeCode.authToken) and CLAUDE_CONCURRENCY.

Every other leaf takes the mechanical form: split camelCase words, uppercase the path, replace the dots with underscores, prefix with KRANTHO_.

PathVariable
llm.openai.apiKeyKRANTHO_LLM_OPENAI_API_KEY
assistant.providerKRANTHO_ASSISTANT_PROVIDER
persistence.sqlite.busyTimeoutMsKRANTHO_PERSISTENCE_SQLITE_BUSY_TIMEOUT_MS
auth.oidc.adminEntryKRANTHO_AUTH_OIDC_ADMIN_ENTRY
media.vercelBlob.tokenKRANTHO_MEDIA_VERCEL_BLOB_TOKEN

Types follow the leaf:

  • Integers must parse as integers. KRANTHO_MEDIA_MAX_SIZE=abc fails the boot.
  • Booleans accept true/false, 1/0, yes/no, on/off.
  • Lists are comma-separated and trimmed: KRANTHO_SITE_SUPPORTED_LOCALES=en,nb,de.
  • A variable set to an empty string counts as unset.

Secrets#

The secret leaves are auth.token.secret, auth.jwtSecret, auth.oidc.clientSecret, llm.anthropic.apiKey, llm.openai.apiKey, llm.gemini.apiKey, llm.openrouter.apiKey, llm.claudeCode.authToken, secrets.local.key, persistence.postgres.url, cache.cdn.gcp.accessToken, and the media store credentials media.vercelBlob.token, media.s3.secretAccessKey and media.gcs.credentials.

Each also accepts a _FILE suffix naming a file to read the value from. That is how a container platform mounts a secret without putting it in the process environment:

KRANTHO_LLM_ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic-key

The variable itself wins when both are set. Secrets are excluded from anything serialized to a browser by construction, and any dump of the tree replaces them with [redacted].

Language model settings at runtime#

The runtime reads the provider, model and base URL from the site settings store, not from the tree. On first boot the server seeds them into that store — only when the code passes an llm block, or the environment chooses a provider (KRANTHO_LLM_PROVIDER) and supplies that provider's key — and it never overwrites a stored provider. A key alone seeds nothing: a provider is on because it has a key, and the site uses the first one that is on when none was chosen. No credential is ever seeded or copied into the database: a key from the environment or the code stays where the deployment put it.

After that, Studio's Setup › AI provider wins for the provider, and for each provider's model and base URL. The credential is picked on every request, and every provider has one of its own. The claude-code provider uses KRANTHO_LLM_CLAUDE_CODE_OAUTH_TOKEN only, and refuses to run in production. Every other provider uses its own variable (KRANTHO_LLM_ANTHROPIC_API_KEY, KRANTHO_LLM_OPENAI_API_KEY, KRANTHO_LLM_GEMINI_API_KEY or KRANTHO_LLM_OPENROUTER_API_KEY) or the key the code passes, then the key pasted in Studio, and never another provider's credential — so a key rotated in the environment takes effect at once. Ollama needs none. krantho reads neither KRANTHO_LLM_API_KEY nor CLAUDE_CODE_OAUTH_TOKEN. See @krantho/ai. The build-time tools read the tree directly, so they always see the current value.

A key pasted in Studio is sealed before it reaches the database: AES-256-GCM under a key derived from secrets.local.key (KRANTHO_SECRETS_LOCAL_KEY) or, when that is unset, from auth.jwtSecret, either at least 32 characters. Only the running system can open it, so a database dump or backup reveals nothing. A site with neither refuses a pasted key and says so; set the provider's own variable instead. Rotating the key makes the stored keys unreadable, and they are entered again. Stored secrets are not part of any import or export.

Which settings a model takes differs by model, so llm.profiles sets them per model id over the defaults: reasoningEffort, reasoningHeadroom and temperature. See @krantho/ai.

Webhooks#

webhooks is a list of targets krantho POSTs to when content changes. Each takes an absolute http or https url, an events list with at least one of create, update, publish and delete, and an optional secret that signs the delivery with HMAC-SHA256 in the X-Webhook-Signature header (t=<timestamp>,v1=<signature>). A list of objects has no environment variable, so read the secret from the environment in krantho.config.ts:

krantho.config.ts
export default defineKranthoConfig({
  // ...
  webhooks: [
    {
      url: 'https://hooks.example.com/rebuild',
      events: ['publish', 'delete'],
      secret: process.env.KRANTHO_WEBHOOK_SECRET,
    },
  ],
})

Each delivery is logged and shows in Setup › Integrations. A failed delivery is retried automatically, backing off from 30 seconds to an hour, up to five attempts; after that it stays failed until someone chooses Retry. The retry runs from the server's request handler, at most every 30 seconds and never in the way of a request, so a site with no traffic retries on its next visit. Deliveries are logged on SQLite and Postgres; on the filesystem adapter there is no log, so they are fire-and-forget and never retried. A webhook's secret is never serialized to a browser and is replaced by [redacted] in any dump of the tree.

Validation rules that span two leaves#

  • site.defaultLocale must appear in site.supportedLocales.
  • auth.mode: 'token' requires auth.token.secret.
  • auth.mode: 'oidc' requires auth.oidc.issuer, clientId and redirectUri.
  • auth.oidc.adminEntry: 'redirect' (with auth.mode: 'oidc') requires auth.jwtSecret.
  • auth.session.sameSite: 'none' requires auth.session.secure: 'always'.
  • persistence.provider: 'postgres' requires persistence.postgres.url.
  • seo.enrichment.meta.minChars must not exceed seo.enrichment.meta.maxChars.
  • cache.pages: 'cdn' requires cache.cdn.provider other than none.
  • cache.cdn.provider: 'cloudfront' requires cache.cdn.cloudfront.distributionId.
  • cache.cdn.provider: 'gcp' requires cache.cdn.gcp.project and cache.cdn.gcp.urlMap.
  • A media.provider requires persistence.provider sqlite or postgres.
  • media.provider: 'vercel-blob' requires media.vercelBlob.token.
  • media.provider: 's3' requires media.s3.bucket and media.s3.region; a media.s3.endpoint requires media.s3.publicUrl; media.s3.accessKeyId and media.s3.secretAccessKey are set together or not at all.
  • media.provider: 'gcs' requires media.gcs.bucket.

In production (NODE_ENV=production), the server also logs a warning at startup for auth.mode: 'none', and for any other mode without auth.jwtSecret. These are warnings, not errors.

Where the configuration is read#

Everything that reads the tree resolves the object defineKranthoConfig() returned against process.env: the API handler, the content loader, the page cache rules and the admin handler. Pass them the same object.

One process cannot see krantho.config.ts: the shared Next.js cache handler (cache.shared: 'database'), which Next.js loads by path. It opens the database from the defaults and the environment alone, reading only paths.* and persistence.*. If your database is not the default, set it by environment (KRANTHO_PERSISTENCE_PROVIDER, KRANTHO_PERSISTENCE_POSTGRES_URL, KRANTHO_PATHS_DATA_DIR). See Rendering and caching.

Loading a .env file in development#

Krantho does not read .env files itself — the process environment is the interface.

Next.js loads .env from the project directory into the environment before it reads next.config.ts, so next dev needs nothing more for a .env beside package.json. A file elsewhere, such as one shared by a monorepo, needs loading into the environment before next dev starts. Do not add Node's --env-file flag to it: next dev passes its Node flags to the server it starts through NODE_OPTIONS, and Node 26 refuses --env-file there.

Vite does not put .env into process.env for server code, so a SvelteKit or Express project lets Node load the file before the dev server starts:

package.json
{
  "scripts": {
    "dev": "node --env-file-if-exists=.env node_modules/vite/bin/vite.js dev"
  }
}

Every setting#

These tables are generated from the configuration schema by envReference() in @krantho/config. Secret marks a leaf that also accepts a _FILE variable; code only marks a structured leaf with no variable.

Site identity#

VariablePathTypeDefaultDescription
KRANTHO_SITE_DOMAINsite.domainstringunsetBare hostname of the site.
KRANTHO_SITE_NAMEsite.namestringunsetHuman-readable site name.
KRANTHO_SITE_LANGUAGEsite.languagestringunsetPrimary content language as a locale code.
KRANTHO_SITE_BASE_URLsite.baseUrlstringunsetAbsolute base URL used for canonical links, the sitemap and robots.
KRANTHO_SITE_SUPPORTED_LOCALESsite.supportedLocalescomma-separated listthe primary language aloneLocales the site publishes. Defaults to the primary language alone.
KRANTHO_SITE_DEFAULT_LOCALEsite.defaultLocalestringthe primary languageLocale served when a request names none. Defaults to the primary language.

Paths#

VariablePathTypeDefaultDescription
KRANTHO_PATHS_DATA_DIRpaths.dataDirstring.kranthoDirectory for krantho runtime data, relative to the working directory.
KRANTHO_PATHS_CONTENT_DIRpaths.contentDirstringcontentDirectory holding markdown content.
KRANTHO_PATHS_SEO_GENERATED_DIRpaths.seoGeneratedDirstring<paths.contentDir>/seo/generatedDirectory the SEO pipeline writes generated files to. Defaults to seo/generated under the content directory.

Persistence#

VariablePathTypeDefaultDescription
KRANTHO_PERSISTENCE_PROVIDERpersistence.providerstringsqlitePersistence backend: sqlite, postgres or filesystem.
KRANTHO_PERSISTENCE_SQLITE_PATHpersistence.sqlite.pathstring<paths.dataDir>/content.dbSQLite database file. Defaults to the data directory plus content.db.
KRANTHO_PERSISTENCE_SQLITE_BUSY_TIMEOUT_MSpersistence.sqlite.busyTimeoutMsinteger10000How long a statement waits for another connection's write lock.
KRANTHO_PERSISTENCE_SQLITE_WAL_RETRIESpersistence.sqlite.walRetriesinteger10Attempts to switch a new database file into WAL mode. At least one.
KRANTHO_PERSISTENCE_SQLITE_WAL_RETRY_DELAY_MSpersistence.sqlite.walRetryDelayMsinteger50Pause between WAL switch attempts.
KRANTHO_PERSISTENCE_POSTGRES_URLpersistence.postgres.urlstringunsetPostgres connection URL. Required when the provider is postgres. Secret.
KRANTHO_PERSISTENCE_POSTGRES_MAX_CONNECTIONSpersistence.postgres.maxConnectionsintegerunsetConnection pool size per process. Unset uses the driver default of 10; keep it small on serverless.
KRANTHO_PERSISTENCE_POSTGRES_PREPAREpersistence.postgres.preparebooleanunsetUse prepared statements. Unset uses the driver default (on); set false behind a transaction pooler such as PgBouncer or a pooled Neon URL.
KRANTHO_PERSISTENCE_POSTGRES_IDLE_TIMEOUT_SECpersistence.postgres.idleTimeoutSecintegerunsetSeconds before an idle pooled connection is closed. Unset keeps idle connections open.
KRANTHO_PERSISTENCE_FILESYSTEM_CONTENT_PATHpersistence.filesystem.contentPathstring<paths.contentDir>Content root for the filesystem adapter. Defaults to the content directory.

Content engine#

VariablePathTypeDefaultDescription
KRANTHO_CONTENT_CACHE_ENABLEDcontent.cache.enabledbooleantrueEnable the content cache of the API and of server-rendered pages. With cache.pages next-isr, Next.js caches page reads instead; with cdn, the CDN caches the pages and pages read uncached.
KRANTHO_CONTENT_CACHE_TTL_MScontent.cache.ttlMsinteger300000Cache entry lifetime in milliseconds. A published change reaches server-rendered pages after at most this long, since pages and the API may run in separate processes.
KRANTHO_CONTENT_CACHE_MAX_ENTRIEScontent.cache.maxEntriesinteger100Maximum cached documents.
KRANTHO_CONTENT_AT_NOTATIONcontent.atNotationbooleantrueExpand @field references in markdown bodies.
KRANTHO_CONTENT_WATCHcontent.watchbooleanfalseWatch filesystem collections and sync changes into the database.
KRANTHO_CONTENT_DEFAULT_STATUS_FILESYSTEMcontent.defaultStatus.filesystemstringpublishedStatus given to documents imported from markdown files.
KRANTHO_CONTENT_LIST_MAX_LIMITcontent.list.maxLimitintegerunsetCap on list responses. Unset means a list returns every row.
KRANTHO_CONTENT_SEARCH_MIN_QUERY_LENGTHcontent.search.minQueryLengthinteger2Shortest accepted search query.
KRANTHO_CONTENT_SEARCH_DEFAULT_LIMITcontent.search.defaultLimitinteger200Search results returned when the request names no limit.
KRANTHO_CONTENT_SEARCH_MAX_LIMITcontent.search.maxLimitinteger500Cap on search results.
KRANTHO_CONTENT_VERSIONS_KEEPcontent.versions.keepinteger10Versions retained per document when purging.
KRANTHO_CONTENT_LOCK_LEASE_MScontent.lock.leaseMsinteger300000How long an editing lock is held before it expires.

Page cache#

VariablePathTypeDefaultDescription
KRANTHO_CACHE_PAGEScache.pagesstringnoneHow rendered pages are cached: none (render every request), next-isr (Next.js keeps each page until a content change invalidates it; the page routes must be ISR routes) or cdn (each page carries CDN cache headers and cache tags, and a content change purges the CDN; requires cache.cdn.provider).
KRANTHO_CACHE_CDN_PROVIDERcache.cdn.providerstringnoneCDN in front of the site, purged when content changes: none, vercel, cloudfront or gcp (Cloud CDN).
KRANTHO_CACHE_CDN_S_MAX_AGE_SECcache.cdn.sMaxAgeSecinteger86400Seconds the CDN keeps a page when no content change purges it first.
KRANTHO_CACHE_CDN_STALE_WHILE_REVALIDATE_SECcache.cdn.staleWhileRevalidateSecinteger604800Seconds the CDN may serve a stale page while it fetches a fresh one.
KRANTHO_CACHE_CDN_TAG_HEADERcache.cdn.tagHeaderstringx-krantho-cache-tagResponse header carrying the cache tags with the cloudfront provider. The CloudFront distribution must be configured to read the same header.
KRANTHO_CACHE_CDN_CLOUDFRONT_DISTRIBUTION_IDcache.cdn.cloudfront.distributionIdstringunsetCloudFront distribution to purge. Required when the provider is cloudfront.
KRANTHO_CACHE_CDN_GCP_PROJECTcache.cdn.gcp.projectstringunsetGoogle Cloud project of the URL map. Required when the provider is gcp.
KRANTHO_CACHE_CDN_GCP_URL_MAPcache.cdn.gcp.urlMapstringunsetCloud CDN URL map to purge. Required when the provider is gcp.
KRANTHO_CACHE_CDN_GCP_ACCESS_TOKENcache.cdn.gcp.accessTokenstringunsetOAuth access token for the Compute Engine API. Unset fetches one from the metadata server. Secret.
KRANTHO_CACHE_SHAREDcache.sharedstringnoneWhere Next.js keeps its page and data cache: none (each instance keeps its own) or database (shared by every instance through the persistence backend).

Authentication#

VariablePathTypeDefaultDescription
KRANTHO_AUTHauth.modestringnoneAuthentication mode: none, token or oidc.
KRANTHO_AUTH_REQUIRE_AUTH_FOR_READSauth.requireAuthForReadsbooleanfalseRequire authentication for GET endpoints as well as writes.
KRANTHO_API_TOKENauth.token.secretstringunsetShared secret accepted by token mode. Secret.
KRANTHO_TOKEN_EMAILauth.token.emailstringadmin@localEmail reported for the token-mode identity.
KRANTHO_TOKEN_ROLEauth.token.rolestringadminRole granted to the token-mode identity.
KRANTHO_JWT_SECRETauth.jwtSecretstringunsetSigning secret for session tokens. An ephemeral one is generated when unset. Secret.
OIDC_ISSUER_URLauth.oidc.issuerstringunsetOIDC issuer URL.
OIDC_CLIENT_IDauth.oidc.clientIdstringunsetOIDC client identifier.
OIDC_CLIENT_SECRETauth.oidc.clientSecretstringunsetOIDC client secret, sent on the token exchange (client_secret_post). Unset means a public client (PKCE only). Secret.
OIDC_REDIRECT_URIauth.oidc.redirectUristringunsetOIDC redirect URI registered with the provider.
OIDC_POST_LOGOUT_URIauth.oidc.postLogoutUristring/admin/Where the provider returns the browser after logout.
KRANTHO_AUTH_OIDC_SCOPESauth.oidc.scopescomma-separated listopenid, email, profileScopes requested at the authorize step.
KRANTHO_SIGNUPauth.oidc.signupstringclosedWhether a first-time OIDC login creates a user: open or closed.
KRANTHO_AUTH_OIDC_ADMIN_ENTRYauth.oidc.adminEntrystringloginHow /admin treats a visitor without a session: login shows the admin sign-in page; redirect sends the visitor to the OIDC provider and answers 404 when sign-in is refused.
KRANTHO_ADMIN_EMAILSauth.oidc.adminEmailscomma-separated list[]Emails promoted to admin on login.
KRANTHO_AUTH_OIDC_NEW_USER_ROLEauth.oidc.newUserRolestringviewerRole given to a user created by OIDC signup.
KRANTHO_AUTH_SESSION_ACCESS_TTL_SECauth.session.accessTtlSecinteger900Access token lifetime in seconds.
KRANTHO_AUTH_SESSION_REFRESH_TTL_SECauth.session.refreshTtlSecinteger2592000Refresh token lifetime in seconds.
KRANTHO_AUTH_SESSION_COOKIE_PREFIXauth.session.cookiePrefixstringkranthoPrefix for the session cookie names.
KRANTHO_AUTH_SESSION_SECUREauth.session.securestringautoSecure cookie flag: always, never, or auto — infer it from x-forwarded-proto.
KRANTHO_AUTH_SESSION_SAME_SITEauth.session.sameSitestringlaxSameSite cookie policy: lax, strict or none. none requires auth.session.secure always.
KRANTHO_AUTH_TRUST_PROXYauth.trustProxybooleanfalseWiden what auth.session.secure "auto" accepts as proof of HTTPS to the other headers a terminating proxy may send.

Media and images#

VariablePathTypeDefaultDescription
KRANTHO_MEDIA_PROVIDERmedia.providerstringunsetWhere media files are stored: local, vercel-blob, s3 or gcs. Unset turns media off. Media needs sqlite or postgres persistence.
KRANTHO_MEDIA_LOCAL_DIRmedia.local.dirstringpublic/uploadsDirectory the local store writes files to. The host framework must serve it at media.local.urlPrefix.
KRANTHO_MEDIA_LOCAL_URL_PREFIXmedia.local.urlPrefixstring/uploadsURL prefix the host framework serves the local store directory under.
KRANTHO_MEDIA_VERCEL_BLOB_TOKENmedia.vercelBlob.tokenstringunsetRead-write token of the Vercel Blob store. Required when the provider is vercel-blob. Secret.
KRANTHO_MEDIA_S3_BUCKETmedia.s3.bucketstringunsetS3 bucket name. Required when the provider is s3.
KRANTHO_MEDIA_S3_REGIONmedia.s3.regionstringunsetS3 region, such as eu-north-1, or auto for Cloudflare R2. Required when the provider is s3.
KRANTHO_MEDIA_S3_ENDPOINTmedia.s3.endpointstringunsetEndpoint URL of an S3-compatible service such as Cloudflare R2 or MinIO. Unset uses AWS. When set, media.s3.publicUrl is required.
KRANTHO_MEDIA_S3_FORCE_PATH_STYLEmedia.s3.forcePathStylebooleanfalseAddress the bucket in the URL path rather than the host name, as MinIO needs.
KRANTHO_MEDIA_S3_PUBLIC_URLmedia.s3.publicUrlstringhttps://<media.s3.bucket>.s3.<media.s3.region>.amazonaws.com when media.s3.endpoint is unsetPublic base URL objects are served from, such as a CDN or bucket domain. Objects must be publicly readable there.
KRANTHO_MEDIA_S3_ACCESS_KEY_IDmedia.s3.accessKeyIdstringunsetAccess key ID. Set it together with media.s3.secretAccessKey; unset both to use the AWS default credential chain.
KRANTHO_MEDIA_S3_SECRET_ACCESS_KEYmedia.s3.secretAccessKeystringunsetSecret access key paired with media.s3.accessKeyId. Secret.
KRANTHO_MEDIA_GCS_BUCKETmedia.gcs.bucketstringunsetGoogle Cloud Storage bucket name. Required when the provider is gcs.
KRANTHO_MEDIA_GCS_PUBLIC_URLmedia.gcs.publicUrlstringhttps://storage.googleapis.com/<media.gcs.bucket>Public base URL objects are served from, such as a CDN in front of the bucket. Objects must be publicly readable there.
KRANTHO_MEDIA_GCS_CREDENTIALSmedia.gcs.credentialsstringunsetService-account key JSON. Unset uses the metadata server of Cloud Run, GKE or Compute Engine. Secret.
KRANTHO_MEDIA_MAX_SIZEmedia.maxFileSizeinteger10485760Largest accepted upload in bytes.
KRANTHO_MEDIA_ALLOWED_MIME_TYPESmedia.allowedMimeTypescomma-separated listimage/jpeg, image/png, image/gif, image/webp, image/avif, image/svg+xmlUpload MIME types accepted.
KRANTHO_MEDIA_IMAGE_MAX_WIDTHmedia.image.maxWidthinteger2048Uploads wider than this are downscaled.
KRANTHO_MEDIA_IMAGE_QUALITY_JPEGmedia.image.quality.jpeginteger82JPEG variant quality.
KRANTHO_MEDIA_IMAGE_QUALITY_WEBPmedia.image.quality.webpinteger80WebP variant quality.
KRANTHO_MEDIA_IMAGE_QUALITY_AVIFmedia.image.quality.avifinteger65AVIF variant quality.
KRANTHO_MEDIA_IMAGE_QUALITY_NORMALIZEmedia.image.quality.normalizeinteger85Quality used when re-encoding an upload.
code onlymedia.image.presetsstructured{"xs":48,"sm":96,"md":256,"lg":512,"xl":1024,"full":null}Preset name to pixel width.
KRANTHO_IMAGE_CACHE_MAX_ENTRIESmedia.image.cache.maxEntriesinteger1000Image variants held in memory.
KRANTHO_IMAGE_CACHE_MAX_MBmedia.image.cache.maxMbinteger256Memory budget for the image variant cache.
KRANTHO_IMAGE_CACHE_TTI_SECSmedia.image.cache.ttiSecinteger3600Idle seconds before a cached variant is dropped.

Language model#

VariablePathTypeDefaultDescription
KRANTHO_LLM_PROVIDERllm.providerstringanthropicLLM provider used for enrichment: anthropic, claude-code, openai, gemini, openrouter or ollama. Each provider has its own credential below; ollama needs none.
KRANTHO_LLM_MODELllm.modelstringunsetModel identifier. Unset uses the provider's preset default.
KRANTHO_LLM_ANTHROPIC_API_KEYllm.anthropic.apiKeystringunsetAPI key for the anthropic provider. Secret.
KRANTHO_LLM_OPENAI_API_KEYllm.openai.apiKeystringunsetAPI key for the openai provider. Secret.
KRANTHO_LLM_GEMINI_API_KEYllm.gemini.apiKeystringunsetAPI key for the gemini provider. Secret.
KRANTHO_LLM_OPENROUTER_API_KEYllm.openrouter.apiKeystringunsetAPI key for the openrouter provider. Secret.
KRANTHO_LLM_CLAUDE_CODE_OAUTH_TOKENllm.claudeCode.authTokenstringunsetThe token claude setup-token prints, used only by the claude-code provider, for development on your own machine. No other provider reads it; deployments use an API key. Secret.
KRANTHO_LLM_BASE_URLllm.baseUrlstringunsetOverride the provider's base URL for a proxy or self-hosted endpoint.
code onlyllm.profilesstructured{}How requests to one model are shaped, by model id, over the default for that model: reasoningEffort (openai only; default leaves it to the model), reasoningHeadroom (output tokens added for the thinking a model does) and temperature. Models such as the gpt-5 family and the o-series reject a temperature and need their reasoning kept short, so the defaults ask for the lowest effort each accepts.

Stored secrets#

VariablePathTypeDefaultDescription
KRANTHO_SECRETS_PROVIDERsecrets.providerstringlocalThe vault that encrypts the secrets a site stores in its database, such as a pasted LLM key. Only local is available: it encrypts with a key from the environment.
KRANTHO_SECRETS_LOCAL_KEYsecrets.local.keystringunsetKey the local vault encrypts stored secrets with, at least 32 characters. Unset uses auth.jwtSecret; with neither, a site stores no secrets in its database. Secret.

Admin assistant#

VariablePathTypeDefaultDescription
KRANTHO_ASSISTANT_ENABLEDassistant.enabledbooleanfalseTurn on the admin assistant. It needs sqlite or postgres and an LLM configured in Setup › AI provider.
KRANTHO_ASSISTANT_PROVIDERassistant.providerstringunsetProvider for the assistant, one of the llm.provider values, using that provider's own credential. Unset uses the provider chosen in Setup › AI provider.
KRANTHO_ASSISTANT_MODELassistant.modelstringunsetModel for the assistant. Unset uses the model chosen in Setup › AI provider, or the provider's default model when assistant.provider is set.
KRANTHO_ASSISTANT_ROLEassistant.rolestringeditorThe lowest role that may use the assistant: viewer, editor or admin. It decides who can spend the site's LLM credential.
KRANTHO_ASSISTANT_CONFIRM_TTL_SECassistant.confirmTtlSecinteger600Seconds a proposed action can be confirmed in the chat. At most 3600.
KRANTHO_ASSISTANT_LIMITS_MAX_TURNSassistant.limits.maxTurnsinteger8Model calls in one chat request.
KRANTHO_ASSISTANT_LIMITS_MAX_TOOL_CALLSassistant.limits.maxToolCallsinteger16Tool executions in one chat request.
KRANTHO_ASSISTANT_LIMITS_MAX_TOKENSassistant.limits.maxTokensinteger16000Output token cap for each model call.
KRANTHO_ASSISTANT_LIMITS_MAX_REQUEST_TOKENSassistant.limits.maxRequestTokensinteger200000Input plus output tokens across all model calls of one chat request.
KRANTHO_ASSISTANT_LIMITS_MAX_HISTORY_MESSAGESassistant.limits.maxHistoryMessagesinteger40Conversation messages accepted from the admin in one chat request.
KRANTHO_ASSISTANT_LIMITS_MAX_INPUT_CHARSassistant.limits.maxInputCharsinteger60000Characters accepted from the admin across the conversation.
KRANTHO_ASSISTANT_LIMITS_TOOL_RESULT_CHARSassistant.limits.toolResultCharsinteger12000Characters of one tool result sent to the model.

SEO#

VariablePathTypeDefaultDescription
KRANTHO_SEO_IDENTITYseo.identitystringunsetSchema.org type describing the site.
code onlyseo.collectionsstructured{}Collection name to Schema.org types.
code onlyseo.routesstructured{}URL pattern to Schema.org types.
code onlyseo.sitestructuredunsetRich site-level SEO configuration.
code onlyseo.richCollectionsstructured{}Rich per-collection SEO configuration.
code onlyseo.richRoutesstructured{}Rich per-route SEO configuration.
KRANTHO_SEO_SEED_MODEseo.seedModestringseedseed preserves rows already in the database; overwrite replaces them.
KRANTHO_SEO_CONFIG_DIRseo.configDirstringunsetDirectory of SEO seed files, relative to the working directory.
KRANTHO_SEO_INDEX_NOW_KEYseo.indexNow.keystringunsetIndexNow key. When set, the site serves it at /.txt and notifies IndexNow (Bing and other participating engines) of published, changed and removed pages.
KRANTHO_SEO_ENRICHMENT_META_MIN_CHARSseo.enrichment.meta.minCharsinteger120Shortest acceptable generated meta description.
KRANTHO_SEO_ENRICHMENT_META_MAX_CHARSseo.enrichment.meta.maxCharsinteger155Longest acceptable generated meta description.
KRANTHO_SEO_ENRICHMENT_META_CONTENT_CAPseo.enrichment.meta.contentCapinteger2000Characters of body text sent to the model for a meta description.
KRANTHO_SEO_ENRICHMENT_META_MAX_TOKENSseo.enrichment.meta.maxTokensinteger1024Token budget for a meta description completion.
KRANTHO_SEO_ENRICHMENT_ARCHETYPE_CONTENT_CAPseo.enrichment.archetype.contentCapinteger3000Characters of body text sent to the model for archetype enrichment.
KRANTHO_SEO_ENRICHMENT_ARCHETYPE_MAX_TOKENSseo.enrichment.archetype.maxTokensinteger2048Token budget for an archetype completion.

Webhooks#

VariablePathTypeDefaultDescription
code onlywebhooksstructured[]Outgoing webhooks, a list of { url, events, secret? }: url is an absolute http(s) URL, events lists at least one of create, update, publish and delete, and secret signs each delivery with HMAC-SHA256. Read a secret from the environment inside krantho.config.ts, for example secret: process.env.KRANTHO_WEBHOOK_SECRET. Deliveries are logged and retried automatically on SQLite and Postgres; on the filesystem adapter there is no log, so deliveries are fire-and-forget.

Build-time tools#

VariablePathTypeDefaultDescription
CLAUDE_CONCURRENCYtools.concurrencyinteger5Parallel model calls made by the build-time pipeline.
KRANTHO_TOOLS_ENRICH_SERVICE_CAPtools.enrich.serviceCapinteger1500Characters of a service document sent to the model.
KRANTHO_TOOLS_ENRICH_LLMS_DESCRIPTION_CAPtools.enrich.llmsDescriptionCapinteger300Longest generated llms.txt link description.
KRANTHO_TOOLS_ENRICH_FAQ_MIN_PER_ROUTEtools.enrich.faqMinPerRouteinteger5FAQ entries a route must have before it is considered complete.
KRANTHO_TOOLS_ENRICH_SKIP_FIELDStools.enrich.skipFieldscomma-separated listdescription, meta-description, page-description, hero-image-alt, footer-title, footer-description, footer-copyright-text, nav-home, nav-about, nav-services, nav-contact, form-*, meta-description-*Frontmatter fields left out of the text the enrichment tools send to the model. A trailing * matches every field with that prefix. Site-specific fields belong in the project's own krantho.config.ts.

Questions and answers

How do editors sign in to the admin?

With an access token, or through OIDC with any provider that supports it. Under OIDC you choose whether a first sign-in creates a user, which role new users get, and which email addresses become administrators. /admin can send a visitor without a session straight to the provider, and answer 404 when sign-in is refused.

Users have one of three roles: admin, editor or viewer. The default mode, none, treats every request as an administrator and is meant only for development on your own machine.

Read the documentation
Does krantho support content in several languages?

Yes. The site declares the locales it publishes and its default locale in its configuration. Each locale of a document is stored and published on its own, the API filters by locale, and page reads take a locale, falling back to the site's default when none is given. The admin has locale chips in the editor, and its Setup › Locales shows which documents are missing a language.

How locales appear in your URLs is up to your routes.

Read the documentation