Skip to content

System Admin Guide

KUKAN supports two deployment methods, described below in order: the simpler Docker Compose (on-premises / isolated networks) first, then AWS (cloud / production).

Deploy on-premises or in isolated networks using Docker Compose. For production use, we recommend the latest release (vX.Y.Z tag).

ターミナルウィンドウ
git checkout vX.Y.Z # latest release
cp .env.example .env
# Generate the auth secret
sed -i.bak "s|^BETTER_AUTH_SECRET=.*|BETTER_AUTH_SECRET=$(openssl rand -base64 32)|" .env && rm .env.bak
cp .env.prod.example .env.prod
# Edit .env.prod
docker compose --env-file .env --env-file .env.prod --profile prod up -d --build

Check the release notes for breaking changes and migration steps before upgrading.

ターミナルウィンドウ
git fetch --tags
git checkout vX.Y.Z # the new release
docker compose --env-file .env --env-file .env.prod --profile prod up -d --build

Database migrations are applied automatically when the worker starts. An older image cannot always be brought back against a migrated database.

On-premises deployments use the following alternative services:

AWS Service On-Premises Alternative
S3 MinIO-compatible (Compose default: pgsty/silo)
OpenSearch PostgreSQL full-text search

KUKAN deploys on AWS as CloudFront (WAF + cache) → internal ALB → ECS Fargate. Deployment is automated with CDK Pipelines (AWS CodePipeline): using a CodeConnections (GitHub App) source, a push to the target branch deploys each environment (dev / prd, etc.) as a CDK Stage.

Environments are defined in infra/config/environments.ts (copied from environments.example.ts). Create environments in the multi-site shape (declare sites). Even with a single site, starting with one sites entry makes adding sites later non-destructive (see Multi-Site Operation). The single-site shape — no sites, with site-specific settings (domain, WAF, etc.) directly on the env entry — keeps working for backward compatibility but should not be used for new deployments (the two shapes cannot be mixed). See the Environment Configuration Reference for the full parameter list.

Key AWS resources:

  • CloudFront — CDN, WAF, TLS termination (the front layer)
  • ALB (internal) — load balancer (reachable only via the CloudFront VPC origin)
  • ECS Fargate — Web application + Pipeline Worker
  • Aurora Serverless v2 / RDS — PostgreSQL database (the job queue lives here too)
  • S3 — File storage
  • Cloud Map — how the web finds the worker to wake it (one private DNS namespace per environment)
  • OpenSearch — Full-text search engine
  • AWS Backup — Isolated backups (enabled on the large scale etc.)

Setting deployNotification: true on an environment posts the outcome to Slack every time that environment finishes deploying. An environment that omits it gets none of the resources.

To get a webhook URL: in Slack app settings, Create New App (From scratch), turn on Incoming Webhooks, then Add New Webhook to Workspace and pick the channel.

The stack creates the box it goes into. Deploying with deployNotification: true outputs the secret’s ARN on the pipeline stack as Notify<Environment>SlackWebhookSecretArn:

ターミナルウィンドウ
# Read the ARN (<Environment> is the environments.ts key, capitalised — e.g. Demo)
aws cloudformation describe-stacks --stack-name KukanPipeline \
--query "Stacks[0].Outputs[?contains(OutputKey, 'Notify<Environment>SlackWebhookSecretArn')].OutputValue" \
--output text
# Paste the webhook URL into it, once
aws secretsmanager put-secret-value --secret-id <the ARN> \
--secret-string 'https://hooks.slack.com/services/...'

Until then notifications do nothing, and the deploy still succeeds. A message carries the outcome, the deployed commit, the sites that were updated (linked when they set domainName), a link to the release notes on a release, and a link to the pipeline execution.

Data-protection settings are integrated into the scale presets:

Setting small medium large
S3 versioning (accidental delete/overwrite guard) — ✓ ✓
DB automated backup retention (point-in-time) 7d 14d 35d
AWS Backup (isolated custody, long-term) — — daily 35d + monthly 12mo

Uploaded files are stored in S3 and automatically replicated across multiple availability zones in the region (11-nines design durability). With versioning enabled, accidental deletes and overwrites are also recoverable. AWS Backup keeps copies of S3 and the database in an isolated Backup Vault, covering incidents that take out the resource itself and generations beyond 35 days. Tune per environment via overrides.backup in environments.ts (enabling awsBackup requires s3Versioning: true).

Objects in storage are separated by prefix according to how they are written. They all live in the same bucket, so one backup configuration covers them.

Prefix What gets written there
resources/ A resource’s canonical bytes; a new object on every replacement
previews/ Preview Parquet and the text used by the search index
lake/ Catalogs and data files behind row diffs (DuckLake, per resource)

The prefix does not decide an object’s role. Which object is the current content and which is a past version is decided by the database pointers (resource.storage_key / resource_version.storage_key). At the moment an object is written it is not yet known whether it will become a version at all — content fetched unchanged produces none.

  • The current file and every past version’s file are all under resources/. That is where storage grows, by one object per replacement
  • resources/ also holds uploads that never completed, and objects awaiting reclamation

The catalogs behind row diffs are SQLite files, one per resource, under lake/<resource ID>/ (no additional environment variables).

Do not exclude any of these prefixes from backups. previews/ and lake/ are derived data that can be rebuilt from the canonical files. When a catalog in lake/, or a file it reads, is missing, the resource’s next load or diff view has it rebuilt from the canonical files. There is currently no supported procedure for regenerating other derived data that has been lost.

CloudFront access logs are always on, for every site (except in an environment placed in an opt-in region such as af-south-1 or ap-east-1: CloudFront cannot deliver logs there, so none are created and synth warns). Each site writes to its own bucket, and logs expire after cdnLogRetentionDays (default 90). The bucket name is in the stack output CdnLogBucketName.

Each line is one request, with among others: the client IP (c_ip), User-Agent, URI and query string, status, time taken (time_taken), and whether the cache served it (x_edge_result_type: Hit / Miss / RefreshHit / Error …). Cookies are not logged (they carry the session token).

  • Logs usually arrive within minutes to an hour
  • Times are in UTC
  • The User-Agent and query string are URL-encoded (url_decode in Athena restores them)

The stack also creates an Athena table for each site and one workgroup for the environment. Their names are in the stack outputs:

Stack output Example (single site) Multiple sites
CdnLogWorkGroup kukan-prd-logs kukan-prd-logs (all sites)
CdnLogTable kukan_prd_logs.cloudfront kukan_prd_<site>_logs.cloudfront

In the Athena query editor, pick that workgroup and query the table. Which site’s log you read is set by the database name in FROM. Query results go to Athena-managed storage, so no results bucket is needed.

Counting clients and cache results during a burst:

-- Times are UTC: JST 10:20–10:25 is 01:20–01:25
SELECT c_ip, url_decode(cs_user_agent) AS ua, x_edge_result_type, count(*) AS n
FROM kukan_prd_logs.cloudfront
WHERE "date" = DATE '2026-01-15' AND time BETWEEN '01:20:00' AND '01:25:00'
GROUP BY 1, 2, 3
ORDER BY n DESC
LIMIT 20;

The only cost is S3 (storage and PUTs; delivery is free). At tens to a hundred bytes per request gzipped, a million requests a month comes to a few cents. Athena bills by data scanned ($5 / TB).

WAF logs and CloudFront’s additional metrics (cache hit rate etc., paid) are not enabled. The cache result and time taken are in the access log, and the hit rate can be counted from it.

Setup (one-time manual; after that, pushes deploy automatically)

Section titled “Setup (one-time manual; after that, pushes deploy automatically)”
  1. Prepare your environment definitions (cp infra/config/environments.example.ts infra/config/environments.ts, then edit environments.ts). Each entry becomes one environment = one pipeline, so delete the ones you don’t need (e.g. remove dev for a single environment)
  2. Create a CodeConnections connection in the AWS console and set its ARN as the top-level connectionArn export in environments.ts (shared by all envs)
    • Open CodeBuild → Settings → Connections → Create connection in the left nav (a shared “Developer Tools” setting; reaching it via CodeBuild is easiest) → select GitHub → name it (e.g. kukan-github, an AWS-side label) → Connect to GitHub → install/authorize the AWS Connector for GitHub app (choose Only select repositories and grant just the repo you deploy from — least privilege) → once status is Available, copy the ARN
    • The GitHub App authorization requires a browser action (creating via CLI/CDK leaves it PENDING until authorized in the browser)
  3. cdk bootstrap (prerequisite for deploy; per account/region, include us-east-1 for the GlobalStack)
  4. For a custom domain / WAF only: create the us-east-1 certificate/WAF once via standalone (npx cdk deploy -c env=prd Prd/KukanGlobalStack) and set certificateArn/webAclArn on each site entry
  5. Commit environments.ts and cdk.context.json so the CodeBuild synth reads them from the checkout
  6. Deploy the pipeline stack once (npx cdk deploy KukanPipeline)
  7. After that, pushes to each env’s deployBranch deploy automatically (the pipeline self-mutates)

Structure of environments.ts (connectionArn is top-level; per-env settings go inside environments):

infra/config/environments.ts
export const connectionArn = 'arn:aws:codeconnections:...:connection/...' // ARN from step 2 (shared by all envs)
export const environments = {
// account is required (misdeployment guard); use the same ID across envs for same-account
dev: {
account: '000000000000',
scale: 'small',
githubRepo: 'owner/repo',
deployBranch: 'develop',
sites: [{ name: 'main', enableWaf: false }],
},
prd: {
account: '000000000000',
scale: 'large',
githubRepo: 'owner/repo',
deployBranch: 'main',
sites: [{ name: 'main', enableWaf: false }],
},
}

For details and design rationale, see docs/specs/en/phase4-deploy.md and ADR-030 (docs/adr/en/030-cdk-pipelines-deploy.md) / ADR-031 (docs/adr/en/031-multi-environment-deploy.md) in the repository.

While no users exist yet, self-registration is forced on and the first registered user automatically becomes a sysadmin. After deploying, open the sign-up page in a browser and register the first user.

For headless initialization, or to recover when no sysadmin is left, use the CLI.

ターミナルウィンドウ
pnpm db:create-user --email admin@example.com --name admin --role sysadmin
Option Description Required
--email Email address Yes
--name Username (lowercase alphanumeric, hyphens, underscores, periods) Yes
--role user or sysadmin (default: user) No

The command prompts for the password (it is not shown, and is asked twice to confirm). It needs 15+ characters and must be hard to guess — see Password strength policy below. It is not taken as a command-line argument, which would stay in the shell history and show in ps. Where there is no terminal (a script, for instance), pass it in the KUKAN_USER_PASSWORD environment variable.

System administrators (sysadmin) can manage users from the admin dashboard. The worker deletes expired sessions every hour.

  • Change user display name and role (user / sysadmin)
  • Cannot demote your own role (lockout prevention)
  • Soft-delete (sets state to deleted)
  • All sessions and API tokens are immediately revoked
  • Audit logs and activity history are preserved
  • A deleted user can be Restored. Purge is also offered, and cannot be undone (related activity and audit logs are anonymized)

Toggle new user self-registration from the admin dashboard (Site Management → User Self-Registration; default: disabled). While disabled, system administrators create users from the admin dashboard. While no users exist, self-registration is enabled regardless of the setting so the instance can be bootstrapped.

Some settings can be changed from the admin dashboard without redeploying (Site Management /dashboard/admin/site and AI Management /dashboard/admin/ai, system administrators only). Changes are stored in the database, recorded in the audit log, and take 30 seconds to a minute to take effect.

Page Setting Description
Site Management Example search queries Example queries shown under the search box
Site Management Site metadata Values that brand components read by key, such as a population and its as-of date (number, text, text per language, date). Every value is readable through the public API. See “Brand customization” for how to read them
Site Management User self-registration See “Self-registration control” above
AI Management Generate AI descriptions in bulk → Language The language AI descriptions are written in (Japanese / English, Japanese by default). See “AI-written descriptions” below
AI Management AI metadata suggestions Feature toggle (kill switch), generation model ID (blank = provider default), and a connection test
AI Management Semantic search → Search settings Whether to include semantically related results in search, and the minimum “closeness” required. Loosening it yields more hits but more noise; tightening favors precision (requires a configuration where an embedding model is available)

Data Reset on Site Management permanently deletes every dataset, resource, organization, group, tag, pipeline job, and associated file (user accounts are kept). It asks you to type RESET to confirm. It cannot be undone, so do not use it in production.

Announcements (/dashboard/admin/announcements) is where you create, edit, and delete the announcements shown on the top page (the newest five). Each takes a category: Info, Maintenance, Release, or Important. Leave the publish date empty to save a draft; set a future date to schedule it.

Semantic search (embeddings) and AI metadata suggestions use the provider chosen by AI_TYPE.

  • AWS: Bedrock (AI_TYPE=bedrock)
  • On-premises / development: Ollama (AI_TYPE=ollama) or OpenAI-compatible (AI_TYPE=openai)
  • Disabled: AI_TYPE=none (NoOp) — semantic search falls back to keyword search, and AI suggestions are unavailable

The AI metadata suggestion model can be switched from the admin dashboard (AI Management → AI metadata suggestions) without redeploying. The embedding model cannot be changed from the dashboard: it is set by AI_EMBEDDING_MODEL (on AWS, bedrock.embeddingModel in environments.ts), and changing it needs a redeploy and regenerated embeddings. Candidate generation models are set via the AI_COMPLETION_MODELS environment variable (comma-separated). This is the list of models approved for the deployment: across all providers it becomes the admin picker options, with the first entry as the default (the dashboard’s “provider default”). A model being available on the server does not put it in the picker — only listed models appear. On Bedrock, CDK injects this variable from bedrock.completionModels in environments.ts (the models granted InvokeModel in IAM), so add candidates there and redeploy. On Ollama, Docker Compose’s ollama-init pulls every listed model at startup, so listed models become available automatically. When unset, only the built-in default model is offered (Ollama: gemma4:e4b / OpenAI-compatible: gpt-4o-mini). To run Ollama on a GPU, see the Getting Started guide.

When AI suggestions don’t work (Bedrock)

Section titled “When AI suggestions don’t work (Bedrock)”

The connection test under AI metadata suggestions on AI Management runs one attempt through the same path as real generation (structured output) and, on failure, shows the error plus a fix hint. Common Bedrock-specific causes:

Symptom (connection-test error) Cause Fix
not authorized to perform: bedrock:InvokeModel The task role lacks IAM access to the model Add it to bedrock.completionModels in environments.ts and redeploy
use case details have not been submitted The Anthropic use-case form hasn’t been submitted Submit the Anthropic “use case details” form in the Bedrock console (once per account; ~15 min to activate)
AccessDenied mentioning aws-marketplace:Subscribe A Marketplace-served model isn’t subscribed on the account Invoke the model once to enable it — see “First-time subscription for Marketplace models” below. If an organization SCP restricts it, grant aws-marketplace:Subscribe / ViewSubscriptions from the management account (an SCP overrides even AdministratorAccess)

All of these are out-of-app AWS setup — one-time console actions. Note that Anthropic models (Claude, etc.) require both the use-case form and a Marketplace subscription — including the default generation model, Claude Haiku 5.5, so a deployment that names no model needs them too — whereas Amazon Titan / Nova need neither (both auto-enable on first invocation).

First-time subscription for Marketplace models

Section titled “First-time subscription for Marketplace models”

AWS Marketplace-served models such as Cohere Embed v4 are not auto-enabled: to make one usable account-wide, a user with AWS Marketplace permissions must invoke it once. A single invocation via either method below enables it for the deployed task roles as well (propagation takes a few minutes).

  • Console: open the model from Bedrock → Model catalog and run any input once in the Playground. Enabled subscriptions are listed under AWS Marketplace → “Manage subscriptions”.

  • CLI (the same thing in one command):

    ターミナルウィンドウ
    aws bedrock-runtime invoke-model --region ap-northeast-1 \
    --model-id cohere.embed-v4:0 --content-type application/json \
    --body '{"texts":["test"],"input_type":"search_document"}' /dev/stdout

If you switch the embedding model, existing data needs the embeddings regenerated to pick it up (see “Reprocessing the search index” below).

Every resource you register can have an AI write a short description from the file’s contents, shown on its resource page. It lets a reader judge what is in a file without opening it, and it feeds search as well (it goes into the per-resource embedding).

One setting turns it on: the model that should write them. Name it and the feature is on; leave it unset and nothing is written and nothing is billed. On AWS it is bedrock.summaryModel in environments.ts; on Docker Compose it is the AI_SUMMARY_MODEL environment variable.

// AWS (infra/config/environments.ts) — CDK injects AI_SUMMARY_MODEL from this
bedrock: {
completionModels: [
'jp.anthropic.claude-haiku-5-5',
'jp.anthropic.claude-sonnet-4-6',
],
summaryModel: 'jp.anthropic.claude-haiku-5-5',
}
ターミナルウィンドウ
# Docker Compose (.env)
AI_SUMMARY_MODEL=jp.anthropic.claude-haiku-5-5

The abstract model must be one from the completion allow-list. The language is the runtime setting ai-summary-locale instead (ja / en, default ja) — language is free, the model is money. Switch it with Language on the bulk-generation card on AI Management (through the API: PUT /api/v1/admin/settings/ai-summary-locale with body {"value":"en"}).

Changing it does not rewrite existing descriptions. Saving recounts the card’s estimate for the new language; to rewrite them, run “Rewrite all” next.

Bulk generation (AI Management → Generate AI descriptions in bulk)

Section titled “Bulk generation (AI Management → Generate AI descriptions in bulk)”

Ordinarily nothing is needed here: descriptions are written as resources are registered. This card exists for resources that predate the setting and for those needing a rewrite because the model, prompt version, or language changed. It never re-fetches an external URL.

Action Covers
Write the missing ones Only resources with no description yet
Rewrite all Those, plus descriptions written by another model, prompt version, or language

Either button states “N resources · estimated X–Y input / Z output tokens (about $low–$high)” before you press it. The input range has a span because a PDF’s token count follows its page count, which cannot be seen from here (the bounds are one page and the page limit).

Each resource’s embedding is rebuilt as its description is written. Running it twice over the same content costs nothing the second time, because it compares rather than regenerates.

The “Usage and cost” card at the top of AI Management shows the tokens AI calls consumed and what they cost. There is nothing to set up: when AI is on (AI_TYPE other than none), every call is recorded.

Purpose Calls
Writing AI descriptions Writing a description for each resource
Metadata suggestions AI metadata suggestions
Connection test The connection test on the AI metadata suggestions card
Resource embeddings Generating embedding vectors for semantic search
Search query embeddings Searches that use semantic search (including MCP searches)

The card shows:

  • Cost this month and Projected for the month. The projection carries this month’s cost so far to the month’s end at the same rate. It is held back until a day of the month has passed, since one call stretched over thirty days reads as a bill nobody will get.
  • A chart over time, by hour (the last 48 hours), day (this month and the last) or month (the last 12 months), coloured by purpose, model or provider, in cost or tokens. Days and months are cut in the TIME_ZONE environment variable (Asia/Tokyo by default).
  • A table of calls, input, output and cache tokens, and cost.

The cost is an estimate at list price, in USD, when the call was made, before tax and discounts. It is stored with the record, so a rate changed later does not move a month already closed.

Provider Cost
Bedrock From the rate table bundled with KUKAN, including the 1.1× for jp.* inference profiles
Ollama Zero (a local model). Tokens are recorded
OpenAI-compatible Not priced (most compatible servers do not bill per token). Tokens are recorded

A call to a model the rate table does not list is not counted as free: it is shown apart, as calls with no known rate that are not in the cost. The rate table comes with KUKAN releases; if calls with no known rate grow after you start using a new model, update KUKAN.

To check against the actual bill, use AWS Cost Explorer. Bedrock bills each Claude model under its own service name (for example Claude Haiku 5.5 (Amazon Bedrock Edition)).

The same report is available from the API: GET /api/v1/admin/ai-usage?granularity=hour|day|month (system administrators only, day by default).

Indexes and derived data are rebuilt from the admin page for what each one covers: Rebuild metadata search index on Index Management, Regenerate embeddings in the Semantic search card on AI Management, and Reprocess all resources on Resource Processing. The last reruns every resource’s pipeline, and that page follows the runs. Each runs asynchronously through the job queue and proceeds in the background once queued.

Action Rebuilds Fetches external URLs Run it when
Rebuild the search index The metadata index; with the switch on, the content index too (without interpreting again) No Search results look inconsistent. After an upgrade that changed the fields of search documents or how content is indexed (analysis changes need the re-analysis below) (POST /api/v1/admin/reindex-metadata)
Reprocess all resources Extracted text, previews, and the index, from stored files Only ones never fetched Full-text search or previews are missing. Takes time in proportion to the number of resources. Cannot be pressed again while one is running
Regenerate embeddings The vectors semantic search uses No Semantic search was enabled after the fact, or the embedding model changed (POST /api/v1/admin/reindex-embeddings). Unchanged content is skipped. The dashboard also prompts while any resource is outside semantic search (below)
Re-analyse search index The index itself, under a new analysis No The search analysis settings moved on. Only when the page says so (below)
Rebuild the content search index Content indexed in the larger units used before No Search reads content in smaller units. Only when the page says so (below). The same as Rebuild the search index with its switch on

None of this is ordinarily needed: the metadata index updates as datasets are created, updated, and deleted, and text and previews are built when the pipeline processes a file. A sync that was missed is picked up by the hourly re-enqueue (and once after the worker starts).

Upgrading to a release that changed the search analysis settings (the Japanese morphological analysis, or how formats are matched regardless of case) puts a re-analysis notice on the dashboard. The analysis is fixed when an index is created, so an existing index has to be rebuilt before the new analysis takes effect. “Rebuild” above re-sends documents to the same index and leaves its analysis as it was, so this is the only action that applies one.

It only re-reads text already stored — no external URL is fetched and no file is reprocessed. It is a one-time action per upgrade, and the notice disappears once it completes (POST /api/v1/admin/search/reanalyse).

Upgrading to a release that reads content in smaller units (chunks) puts a rebuild the content search index notice on the dashboard. The unit is fixed when content goes into the index, so a resource whose content has not changed stays in the larger units until it is rebuilt. Search still works in the meantime, but content search and passage search get lighter only for the resources that have been rebuilt.

The notice shows how many resources are indexed in the older units. Pressing Rebuild starts what Rebuild the search index on Index Management does with Also rebuild the index of file content switched on: the content of every resource is indexed again from its stored interpretation. No external URL is fetched and no file is interpreted again (except a resource whose interpretation has not caught up with its file).

Until its turn comes, each resource still turns up in dataset search as it is indexed now; passage search (the MCP search_passages) finds it once it is rebuilt. The content of a deleted resource, of a resource in a dataset that is not published, of a format the index takes nothing from, or of one with no stored file is removed from the index rather than rebuilt. The count goes down as the work proceeds, and the notice disappears once it completes. Follow it on Background Job Status, as Content index rebuild (resources).

Background Job Status (/dashboard/admin/queue) lists the jobs in the job queue. Everything that runs in the background — data ingestion, metadata search index sync, embeddings and AI descriptions, purges and more — goes through the job queue.

The table at the top counts jobs by type and status.

  • The columns follow a job’s course: Scheduled → Waiting → Running → Dead. Scheduled is a job whose time has not come yet, including one that failed and is waiting to retry
  • The rows are grouped by what sets each job off: Routine, Admin actions and recovery, and Migrations after an upgrade (plus Other for any type outside them). Each type reads as “work (target)”, such as “Pipeline run (resources)” or “Version deletion (resources)”
  • Pipeline runs an administrator’s button queued are listed under Admin actions and recovery, apart from Routine: “Reprocess of every resource (resources)” and “Content index rebuild (resources)”
  • Clicking a count lists those jobs below (Scheduled when the page opens)

The page refreshes every 5 seconds while the tab is visible, and the refresh button reloads it at once.

A job that fails three times is Dead, and is kept for 30 days (the hourly pass removes it after that). A dead job can be Retried (its attempts reset to zero) or Deleted from the list. Deleting it means its work is not done, except that a version deletion or an organization purge already under way is queued again within the hour. If you delete the removal of a purged dataset’s change-tracking data, the reconciliation tool under “Reclaiming storage” reclaims it.

Without WORKER_WAKE_URL, a job the web queues waits for the worker’s next start or the hourly pass (see “Environment variables” below).

Unlike reprocessing, these run once, at migration time. The dashboard shows how much work is outstanding and the item disappears when it reaches zero.

While any resource sits outside semantic search, the dashboard shows a prompt. Embeddings get one because their absence is silent: with no vector to match against, a search does not fail — it quietly falls back to keywords alone.

Two things raise it. Upgrading to the release that moved the unit of embedding to the resource, which leaves every existing vector needing to be rebuilt, and changing the embedding model. An ordinary edit does not: a resource whose embedding mark has stood for less than 15 minutes is not counted.

The prompt runs the same work as “Regenerate embeddings” in the table above.

Resources that predate versioning have no version. Running Version backfill on the dashboard records each resource’s current file as v1 and loads tabular ones into the row-level diff. Nothing is re-fetched or re-indexed (GET /api/v1/admin/version-backfill-status / POST /api/v1/admin/backfill-versions). It also moves row diffs loaded by an earlier release into a catalog per resource.

For the per-layer breakdown and the conversion of old-style reverts, see the Data Administrator Guide.

Previews written before the OData feed (User Guide) never recorded how many rows sit in the chunk a read has to decode at once (its row group). Without that figure the feed errs on the safe side and cuts each page small, so a table with many columns or long text can be refused as unservable.

While any are left, the dashboard shows Prepare tables for BI tools. Running it reads the stored previews and records the figure. Nothing is fetched from the source again (GET /api/v1/admin/row-group-status / POST /api/v1/admin/record-row-groups).

Some previews written under older settings need more than the figure — their file is rebuilt. That too comes from the stored copy, so nothing is fetched, and the dashboard says how many before you press.

There is no hurry. The feed works without it; it just refuses more tables than it has to.

KUKAN automatically monitors the health of external URL resources.

  • Periodic automated checks via scheduler (by default every 5 minutes, a HEAD request to each external URL not checked in the last 24 hours; changed ones are fetched again, and ones whose changes cannot be told are fetched again every 168 hours)
  • View health check results from the admin dashboard (Health Check)
  • Identify and address failing resources

The intervals are set by HEALTH_CHECK_CRON / HEALTH_CHECK_STALENESS_HOURS / HEALTH_CHECK_FULL_FETCH_INTERVAL_HOURS (see “Environment variables” below).

Objects that nothing points at any more — a file that was replaced, for instance — are recorded in a ledger, and an hourly sweep deletes them once their retention period elapses. The sweep re-checks whether anything still points at each key immediately before deleting it, so normal operation needs no intervention.

The ledger arrived in the same release as version history. Objects stranded by an interrupted operation before that are named by no record and no pointer. This affects only deployments that were running before this release, and finding them means listing the bucket, so it is a manual tool rather than a scheduled job. The same tool reclaims lake/ left behind when a dataset purge’s clean-up job is lost.

Run it inside the deployed container, which is where the production database and bucket are in reach:

ターミナルウィンドウ
node apps/worker/dist/reconcile-orphans.js # report only
node apps/worker/dist/reconcile-orphans.js --commit # record in the ledger

From a checkout:

ターミナルウィンドウ
pnpm --filter @kukan/worker reconcile-orphans
pnpm --filter @kukan/worker reconcile-orphans -- --commit
  • It reports without acting by default. The keys it would nominate are listed, so read them before running with --commit
  • --commit deletes nothing either. It writes ledger rows; the hourly sweep does the deleting, and re-checks each key against the live pointers first. An object whose pointer landed between the listing and the sweep is kept, so a run that turns out to have been wrong costs a row rather than a file
  • Objects written in the last 24 hours are out of scope. A write still in flight is indistinguishable from a leak in a listing, so these are reported as tooRecent instead
  • Repeat until both nominated and tooRecent report zero. One pass may not be enough
  • It covers resources/, previews/ and lake/. In lake/, only a lake/<resource ID>/ whose resource no longer exists (the clean-up job of a dataset purge, lost before it ran). Inside a resource that exists, every write clears up after itself, and lake/main/ (the previous format) is left alone

Key environment variables are listed below. See .env.example for the full list.

Variable Description Default
POSTGRES_HOST PostgreSQL host localhost
POSTGRES_PORT PostgreSQL port 5432
POSTGRES_DB Database name kukan
POSTGRES_USER Database user kukan
POSTGRES_PASSWORD Database password kukan
POSTGRES_SSLMODE TLS to the DB (disable / require / verify-full); verify-full also checks the certificate. On AWS the CDK sets verify-full disable
POSTGRES_SSLROOTCERT Path to the CA bundle verify-full checks against; unset, Node’s trust store. On AWS the CDK sets /etc/ssl/rds-ca.crt -
S3_BUCKET S3 bucket name kukan-dev
S3_ENDPOINT S3 endpoint (MinIO: http://localhost:9000, AWS: omit) -
S3_ACCESS_KEY / S3_SECRET_KEY MinIO credentials (omit on AWS, which uses the IAM role) -
SEARCH_TYPE Search engine (opensearch / postgres) opensearch
OPENSEARCH_URL OpenSearch endpoint http://localhost:9200
WORKER_WAKE_URL Where the web tells the worker a job is waiting (the worker’s /wake). Unset, a job waits for the worker’s next start or the hourly pass http://localhost:8080/wake in development, none otherwise
WORKER_DB_POOL_MAX Upper bound of the worker’s DB connection pool. The default concurrency is derived from it 5
WORKER_CONCURRENCY Jobs the worker runs at once. Unset: WORKER_DB_POOL_MAX − 2 (at most 4). Raise it where memory allows unset (3 with a pool of 5)
WEB_DB_POOL_MAX Upper bound of the web’s DB connection pool 5
BETTER_AUTH_SECRET Auth session secret (required, 32+ characters; generate with openssl rand -base64 32) -
BETTER_AUTH_URL Public URL http://localhost:3000
PASSWORD_MIN_SCORE Password strength floor (zxcvbn score 0–4) — see below 3
CLIENT_IP_HEADER Header the sign-in rate limit reads the client IP from; x-kukan-client-ip only behind CloudFront x-forwarded-for
AI_TYPE AI provider (none / bedrock / openai / ollama) none
AI_EMBEDDING_MODEL Embedding model; changing it needs the embeddings regenerated the provider’s (Bedrock: Titan Text Embeddings v2 / Ollama: bge-m3 / OpenAI-compatible: text-embedding-3-small)
AI_EMBEDDING_DIMENSIONS Embedding dimensions the provider’s
OPENAI_BASE_URL URL of the OpenAI-compatible endpoint (AI_TYPE=openai) -
OLLAMA_URL URL of Ollama (AI_TYPE=ollama) http://localhost:11435
AI_COMPLETION_MODELS Allow-list of generation models (comma-separated); the picker’s options, first is the default -
AI_SUMMARY_MODEL Model that writes AI descriptions. Unset turns the feature off (and costs nothing) — see above -
HEALTH_CHECK_ENABLED Whether external URLs are health-checked true
HEALTH_CHECK_CRON How often health checks run (cron expression) */5 * * * *
HEALTH_CHECK_STALENESS_HOURS An external URL is checked again once this long has passed since its last check 24
HEALTH_CHECK_FULL_FETCH_INTERVAL_HOURS Hours between re-fetches of an external URL whose changes cannot be told 168
LOG_LEVEL Log level (trace–fatal) info
TIME_ZONE IANA zone times are prerendered in; the browser then shows the viewer’s Asia/Tokyo
USER_AGENT_URL Contact the worker names to servers; public sites only (AWS: site’s nameSiteInUserAgent) -

Every route that sets a password — sign-up, a user changing their own, an administrator creating one, pnpm db:create-user — is held to the same two conditions:

  • at least 15 characters (not configurable), and
  • a zxcvbn score of at least PASSWORD_MIN_SCORE (default 3, “safely unguessable”).

Lowering PASSWORD_MIN_SCORE lets common word sequences and keyboard patterns through. Setting it to 0 disables the guessability check altogether.