nicodemus-opon/lunapad

Notebook for analytics engineering where exploratory queries become reusable, production-ready data models

TypeScript

8

115 commits

updated Sep 20, 2026

See the code

README

Lunapad

A notebook-style IDE for PRQL and SQL that runs entirely in the browser. Each notebook cell is a dbt model — cells reference each other by name, and are assembled into a WITH CTE chain at query time with no dbt invocation needed for interactive runs.

Query engines:

  • DuckDB WASM — built-in, zero config, runs in the browser
  • Trino — used for all external data sources (Postgres, ClickHouse, MySQL)

Using Lunapad? See the user guide for a full walkthrough: notebooks, queries, data sources, dashboards, the AI assistant, the API/MCP server, and self-hosting. This README covers running and developing Lunapad itself.


The compose file starts Lunapad, Trino, Postgres, and an Inngest scheduler together. No configuration needed to get started.

# First run (builds the app image, ~3 min)
docker compose up --build

# Subsequent starts
docker compose up -d

# Hosted cloud stack (open signup, RustFS, Mailpit, worker)
docker compose -f docker-compose.cloud.yml up --build

# Public read-only demo (no login, auto-loads sample notebook)
docker compose -f docker-compose.yml -f docker-compose.demo.yml up -d

See docs/guide/11-self-hosting.md for demo deployment details.

ServiceURL
Lunapadhttp://localhost:3000
Trinohttp://localhost:8080
Inngest UIhttp://localhost:8288
Postgreslocalhost:5432

The cloud override serves Lunapad at http://localhost:3967, captures local email at http://localhost:8025, stores published artifacts in bundled RustFS, and runs queued jobs through the bundled worker.

For Coolify, use docker-compose.cloud.yml as the Compose file. It uses named Docker volumes, accepts Coolify-generated service envs, and keeps Postgres, Redis, Trino, and RustFS private on the Docker network.

Startup order: Postgres → Trino (waits for starting: false) → App → Inngest. Trino takes ~60s on first start; the app won't serve until it's ready.

Sample data available immediately — no setup needed:

SELECT * FROM tpch.tiny.nation
SELECT * FROM tpch.tiny.orders LIMIT 100

A docker_postgres catalog is also pre-wired to the bundled Postgres instance (tpch.tiny.* equivalent for Postgres queries).

Useful commands

docker compose logs -f          # tail all services
docker compose logs -f trino    # tail just Trino
docker compose ps               # check health status (all should show "healthy")
docker compose down             # stop, keep data
docker compose down -v          # stop and wipe all data (fresh start)

Adding a data source

  1. Click Data Sources → Add source in the left sidebar
  2. Fill in connection details (Postgres, ClickHouse, or MySQL)
  3. Click Save — Lunapad writes a Trino catalog file and waits for Trino to restart and load it (~15s)

Once saved, reference the source in any cell using catalogName.schema.table:

from docker_postgres.public.users
filter active == true
select {id, email}

The Source ID (catalog name) is set when you create the source and cannot be changed. It must be lowercase letters, digits, and underscores, starting with a letter.


Local development

Requires Node 22, pnpm 9, and a running Trino instance (the Docker compose infra works fine alongside a local dev server).

pnpm install
pnpm dev          # starts on http://localhost:5173

Required environment variables when running outside Docker:

VariableDefaultDescription
TRINO_URLhttp://trino:8080Trino coordinator URL
TRINO_CATALOG_DIR—Path Trino reads catalog .properties files from. For Docker compose local dev: ./trino/catalog
INNGEST_BASE_URL—Inngest dev server URL (omit to disable scheduling)
INNGEST_EVENT_KEY—Set to local for dev
INNGEST_SIGNING_KEY—Set to local for dev
PROJECT_FOLDER—Default dbt project folder, auto-opened on startup. If the folder is empty, a dbt-best-practices project is scaffolded into it automatically.

For local dev pointing at the Docker infra:

TRINO_URL=http://localhost:8080 \
TRINO_CATALOG_DIR=./trino/catalog \
INNGEST_BASE_URL=http://localhost:8288 \
INNGEST_EVENT_KEY=local \
INNGEST_SIGNING_KEY=local \
pnpm dev

All dev commands

pnpm dev          # dev server (requires COOP/COEP headers — handled by vite.config.ts)
pnpm build        # production build
pnpm test         # unit tests (vitest)
pnpm test:e2e     # Playwright e2e tests
pnpm check        # svelte-check type checking
pnpm lint         # prettier --check
pnpm format       # prettier --write

Run a single test file:

pnpm vitest run src/lib/services/cell-deps.test.ts

How cells work

Each cell has an output name (the variable in the top-left input). Cells that reference another cell's output name automatically get it as a CTE dependency:

# cell: orders_clean
from tpch.tiny.orders
filter status == "F"
# cell: summary  (references orders_clean automatically)
from orders_clean
aggregate { total = sum this }

At run time, summary compiles to:

WITH orders_clean AS (
    SELECT * FROM tpch.tiny.orders WHERE status = 'F'
)
SELECT SUM(*) AS total FROM orders_clean

Language modes per cell: PRQL (default), Visual (drag-drop pipeline), SQL. SQL cells on external connections skip PRQL compilation and prepend CTEs directly.


Trino catalog management

Lunapad manages Trino catalogs by writing .properties files to TRINO_CATALOG_DIR (bind-mounted into both the app and trino containers at ./trino/catalog). When a new source is registered:

  1. The catalog .properties file is written to TRINO_CATALOG_DIR
  2. If Trino already has the catalog active, done
  3. Otherwise, a graceful shutdown is triggered (PUT /v1/info/state SHUTTING_DOWN)
  4. Docker's restart: unless-stopped brings Trino back up
  5. App polls /v1/info until starting: false, then verifies the catalog is active

Existing catalog files in ./trino/catalog/ survive restarts. The tpch.properties and docker_postgres.properties files are version-controlled and always present.


dbt integration

Open a dbt project folder via File → Open project, or let the deployment's default folder open automatically (see PROJECT_FOLDER below). When a project is open:

  • Models are compiled with dbt compile and the manifest is used for schema and lineage
  • Run/test buttons appear per-cell for dbt models
  • Scheduled materializations use Inngest functions (dbt run --select model)

In Docker, PROJECT_FOLDER=/app/project (bind-mounted from ./project on the host) is auto-opened on first load; if empty, a dbt-best-practices project (staging/intermediate/marts layout, profiles.yml, etc.) is scaffolded into it automatically. Opening a different folder from the UI always takes precedence over the default on later reloads.

data-engineering
data-science
dbt
duckdb
notebook
prql
sql
trino

nicodemus-opon/lunapad

Notebook for analytics engineering where exploratory queries become reusable, production-ready data models

TypeScript

8

115 commits

updated Sep 20, 2026

See the code

README

Lunapad

A notebook-style IDE for PRQL and SQL that runs entirely in the browser. Each notebook cell is a dbt model — cells reference each other by name, and are assembled into a WITH CTE chain at query time with no dbt invocation needed for interactive runs.

Query engines:

  • DuckDB WASM — built-in, zero config, runs in the browser
  • Trino — used for all external data sources (Postgres, ClickHouse, MySQL)

Using Lunapad? See the user guide for a full walkthrough: notebooks, queries, data sources, dashboards, the AI assistant, the API/MCP server, and self-hosting. This README covers running and developing Lunapad itself.


The compose file starts Lunapad, Trino, Postgres, and an Inngest scheduler together. No configuration needed to get started.

# First run (builds the app image, ~3 min)
docker compose up --build

# Subsequent starts
docker compose up -d

# Hosted cloud stack (open signup, RustFS, Mailpit, worker)
docker compose -f docker-compose.cloud.yml up --build

# Public read-only demo (no login, auto-loads sample notebook)
docker compose -f docker-compose.yml -f docker-compose.demo.yml up -d

See docs/guide/11-self-hosting.md for demo deployment details.

ServiceURL
Lunapadhttp://localhost:3000
Trinohttp://localhost:8080
Inngest UIhttp://localhost:8288
Postgreslocalhost:5432

The cloud override serves Lunapad at http://localhost:3967, captures local email at http://localhost:8025, stores published artifacts in bundled RustFS, and runs queued jobs through the bundled worker.

For Coolify, use docker-compose.cloud.yml as the Compose file. It uses named Docker volumes, accepts Coolify-generated service envs, and keeps Postgres, Redis, Trino, and RustFS private on the Docker network.

Startup order: Postgres → Trino (waits for starting: false) → App → Inngest. Trino takes ~60s on first start; the app won't serve until it's ready.

Sample data available immediately — no setup needed:

SELECT * FROM tpch.tiny.nation
SELECT * FROM tpch.tiny.orders LIMIT 100

A docker_postgres catalog is also pre-wired to the bundled Postgres instance (tpch.tiny.* equivalent for Postgres queries).

Useful commands

docker compose logs -f          # tail all services
docker compose logs -f trino    # tail just Trino
docker compose ps               # check health status (all should show "healthy")
docker compose down             # stop, keep data
docker compose down -v          # stop and wipe all data (fresh start)

Adding a data source

  1. Click Data Sources → Add source in the left sidebar
  2. Fill in connection details (Postgres, ClickHouse, or MySQL)
  3. Click Save — Lunapad writes a Trino catalog file and waits for Trino to restart and load it (~15s)

Once saved, reference the source in any cell using catalogName.schema.table:

from docker_postgres.public.users
filter active == true
select {id, email}

The Source ID (catalog name) is set when you create the source and cannot be changed. It must be lowercase letters, digits, and underscores, starting with a letter.


Local development

Requires Node 22, pnpm 9, and a running Trino instance (the Docker compose infra works fine alongside a local dev server).

pnpm install
pnpm dev          # starts on http://localhost:5173

Required environment variables when running outside Docker:

VariableDefaultDescription
TRINO_URLhttp://trino:8080Trino coordinator URL
TRINO_CATALOG_DIR—Path Trino reads catalog .properties files from. For Docker compose local dev: ./trino/catalog
INNGEST_BASE_URL—Inngest dev server URL (omit to disable scheduling)
INNGEST_EVENT_KEY—Set to local for dev
INNGEST_SIGNING_KEY—Set to local for dev
PROJECT_FOLDER—Default dbt project folder, auto-opened on startup. If the folder is empty, a dbt-best-practices project is scaffolded into it automatically.

For local dev pointing at the Docker infra:

TRINO_URL=http://localhost:8080 \
TRINO_CATALOG_DIR=./trino/catalog \
INNGEST_BASE_URL=http://localhost:8288 \
INNGEST_EVENT_KEY=local \
INNGEST_SIGNING_KEY=local \
pnpm dev

All dev commands

pnpm dev          # dev server (requires COOP/COEP headers — handled by vite.config.ts)
pnpm build        # production build
pnpm test         # unit tests (vitest)
pnpm test:e2e     # Playwright e2e tests
pnpm check        # svelte-check type checking
pnpm lint         # prettier --check
pnpm format       # prettier --write

Run a single test file:

pnpm vitest run src/lib/services/cell-deps.test.ts

How cells work

Each cell has an output name (the variable in the top-left input). Cells that reference another cell's output name automatically get it as a CTE dependency:

# cell: orders_clean
from tpch.tiny.orders
filter status == "F"
# cell: summary  (references orders_clean automatically)
from orders_clean
aggregate { total = sum this }

At run time, summary compiles to:

WITH orders_clean AS (
    SELECT * FROM tpch.tiny.orders WHERE status = 'F'
)
SELECT SUM(*) AS total FROM orders_clean

Language modes per cell: PRQL (default), Visual (drag-drop pipeline), SQL. SQL cells on external connections skip PRQL compilation and prepend CTEs directly.


Trino catalog management

Lunapad manages Trino catalogs by writing .properties files to TRINO_CATALOG_DIR (bind-mounted into both the app and trino containers at ./trino/catalog). When a new source is registered:

  1. The catalog .properties file is written to TRINO_CATALOG_DIR
  2. If Trino already has the catalog active, done
  3. Otherwise, a graceful shutdown is triggered (PUT /v1/info/state SHUTTING_DOWN)
  4. Docker's restart: unless-stopped brings Trino back up
  5. App polls /v1/info until starting: false, then verifies the catalog is active

Existing catalog files in ./trino/catalog/ survive restarts. The tpch.properties and docker_postgres.properties files are version-controlled and always present.


dbt integration

Open a dbt project folder via File → Open project, or let the deployment's default folder open automatically (see PROJECT_FOLDER below). When a project is open:

  • Models are compiled with dbt compile and the manifest is used for schema and lineage
  • Run/test buttons appear per-cell for dbt models
  • Scheduled materializations use Inngest functions (dbt run --select model)

In Docker, PROJECT_FOLDER=/app/project (bind-mounted from ./project on the host) is auto-opened on first load; if empty, a dbt-best-practices project (staging/intermediate/marts layout, profiles.yml, etc.) is scaffolded into it automatically. Opening a different folder from the UI always takes precedence over the default on later reloads.

data-engineering
data-science
dbt
duckdb
notebook
prql
sql
trino

Languages

TypeScript

71.2%

Svelte

25.1%

JavaScript

2.5%

CSS

1.1%