Skip to content

Sheet approval template ​

create-hikoutei ships a self-hostable approval queue built with Next.js (App Router) and Hikoutei:

sh
npm create hikoutei@latest sheet-approval -- --template sheet-approval
cd sheet-approval

Your application reads and writes typed entities against local SQLite. Committed changes are asynchronously projected to a Google Sheet so people can review and lightly collaborate. Hikoutei is not a database replacement and not a raw Sheets API wrapper — SQLite is the authority, Sheets is the human-facing view.

Quick start (local-only, no Google credentials) ​

Authentication is deny-closed: without credentials the app serves no unauthenticated requests until you explicitly opt into the local quick start. Enable it by copying .env.example to .env.local and setting HIKOUTEI_ALLOW_INSECURE_LOCALHOST=true:

sh
cd sheet-approval
npm install
cp .env.example .env.local   # then set HIKOUTEI_ALLOW_INSECURE_LOCALHOST=true
npm run dev                  # binds to 127.0.0.1

Open http://127.0.0.1:3000 in a browser. Create approval requests, change their status, and remove them. None of this contacts Google. Without the opt-in, development also fails closed: next dev rejects every request with 503 until both credentials are configured. The opt-in only applies to development builds and only for localhost / 127.0.0.1 / [::1] request hosts (each with an optional decimal port); a production build never serves unauthenticated requests, even when the environment variable is set.

How writes work ​

flush() commits the entity table, canonical sync state, and the durable Sheet effect outbox in one local SQLite transaction. The Google Sheet projection is delivered asynchronously by the outbox worker. Saving a request therefore means "committed to SQLite", not "written to the Sheet".

Enable the Google Sheets projection (optional) ​

In your local .env.local, uncomment and set:

sh
HIKOUTEI_SYNC_SPREADSHEET_URL=https://docs.google.com/spreadsheets/d/<id>/edit
GOOGLE_APPLICATION_CREDENTIALS=/absolute/path/to/service-account.json

GOOGLE_APPLICATION_CREDENTIALS must point at a real service-account key file on disk; never paste JSON content into the variable. Share the spreadsheet with the service account email as an Editor. People can then inspect and review committed requests in the sheet.

Deploy ​

A local SQLite authority needs a long-lived process and a writable, persistent filesystem.

Authentication is required for any real deployment. The template ships a deny-closed HTTP Basic Auth middleware: unless both AUTH_USERNAME and AUTH_PASSWORD are set, every request is rejected (503 in production). Set both to long random values and always serve over HTTPS. Never put the real values in a client bundle, a log line, or a Sheet.

  • Docker Compose builds the standalone Next.js server and mounts a persistent volume at /data. The Compose file refuses to start without AUTH_USERNAME and AUTH_PASSWORD:

    sh
    AUTH_USERNAME=<long-random-value> AUTH_PASSWORD=<long-random-value> docker compose up --build

    Put a TLS-terminating reverse proxy (Caddy, Traefik, nginx, a platform edge) in front of port 3000 — never expose the app without HTTPS. To enable the Sheet projection, mount the service-account key file read-only into the container and point GOOGLE_APPLICATION_CREDENTIALS at the mounted path (the Compose file ships these commented out):

    yaml
    environment:
      HIKOUTEI_SYNC_SPREADSHEET_URL: https://docs.google.com/spreadsheets/d/<id>/edit
      GOOGLE_APPLICATION_CREDENTIALS: /run/secrets/service-account.json
    volumes:
      - ./service-account.json:/run/secrets/service-account.json:ro
  • Railway uses the provided railway.json, which is SQLite-only by default: a /data volume for the authority file, AUTH_USERNAME and AUTH_PASSWORD as Railway variables (strong values), and Railway's HTTPS domain enabled. The Google Sheets projection is manual and opt-in: provide the service-account key as a real file through a platform-specific secure file provision (for example a volume mount containing the key), then set GOOGLE_APPLICATION_CREDENTIALS to the path of that file inside the container and HIKOUTEI_SYNC_SPREADSHEET_URL to your spreadsheet. Never paste the JSON content into the GOOGLE_APPLICATION_CREDENTIALS variable itself — it must point at a file. Without a secure file provision, leave both sync variables unset and run SQLite-only.

In local development (npm run dev), the app runs without credentials only when you explicitly copied .env.example to .env.local and set HIKOUTEI_ALLOW_INSECURE_LOCALHOST=true; without that opt-in, or for any non-loopback host, development also fails closed with 503 until you set both credentials.

Not supported by this template ​

  • Serverless / read-only filesystems: SQLite needs a writable local volume.
  • Multi-replica: a local SQLite file is not a shared coordination layer; run a single writable instance.
  • Immediate read-after-write in Google Sheets: the projection is asynchronous by design.

Related: Google Sheets as an ops UI.

MIT Licensed