Architecture

SeeKi is one Rust binary. It boots an axum HTTP server on 127.0.0.1:3141, serves a compiled Svelte UI out of the binary itself, and answers a small read-only API backed by a pluggable database pool. No separate web server, no runtime, no build step at install time.

This page is the map. If you are about to add an engine, rework the API, or trace a strange response back to its source, start here.

The ten-second version

  1. A browser asks for /. The binary answers with the embedded index.html.
  2. The UI asks for /api/tables. The binary answers out of the database pool.
  3. There is no step three.

Request flow

A request for /api/tables: router matches, handler reads shared state, pool dispatches to the engine, engine runs SQL, results come back as JSON. Any path that does not match /api/* falls through to the embedded-asset handler.

Two modes, one binary

SeeKi boots into one of two modes. The choice is made at startup by reading seeki.toml:

  • Normal mode — a valid config was found. The database pool is connected, and the full API is available.
  • Setup mode — no config file exists. Only the setup endpoints under /api/setup/* respond; data endpoints return 503.

The mode is held in a shared SharedAppMode (src/app_mode.rs), an Arc<RwLock<AppMode>> that the setup handler flips to Normal once the user saves a config. The binary never needs to restart.

Single-binary delivery

The Svelte UI is compiled to frontend/dist/ at build time, then baked into the Rust binary with rust-embed. The relevant piece lives in src/embed.rs:

#[derive(RustEmbed)]
#[folder = "frontend/dist/"]
struct Assets;

At runtime, any request that does not match the API falls through to the embedded-asset handler, which looks up uri.path() inside Assets, serves the match with a cache header, and falls back to index.html so the SPA can handle its own routing. One executable contains the server, the UI, the fonts, and the favicon.

Module layout

Every file under src/ has one job. Read the file whose job matches the question.

PathPurpose
src/main.rsBoots tracing, loads the config, picks a mode, builds the router, binds a TCP listener.
src/config.rsThe typed shape of seeki.toml: AppConfig, DatabaseConfig, DatabaseKind, SshConfig, display overrides, secrets.
src/app_mode.rsThe AppMode enum (Normal / Setup) and the SharedAppMode handle everything else holds.
src/api/mod.rsRouter wiring and the read-only handlers: tables, columns, rows, display config, CSV export, status.
src/api/setup.rsFirst-run endpoints: test-connection and save. The only code path that writes seeki.toml.
src/db/mod.rsThe DatabasePool enum — the abstraction every engine implements. Shared types: TableInfo, ColumnInfo, QueryResult, ValidationError.
src/db/postgres.rsThe PostgreSQL engine. List tables, describe columns, page rows, stream CSV. Identifier-quoting lives here.
src/ssh/mod.rsThe optional SSH tunnel. Wraps russh, forwards a local port to the database host, and keeps itself alive for the life of the pool.
src/embed.rsThe rust-embed asset handler. Everything not under /api/* ends up here.
src/auth/mod.rsLocalhost-only bind plus CORS predicate. SeeKi has no accounts — the security model is “only this machine can reach it.”

Shared state

Every handler in src/api/mod.rs takes one extension: Extension<SharedAppMode>. Handlers that need the database pool call a tiny helper, require_state, which returns the Arc<AppState> in Normal mode and a 503 in Setup mode. That is the only gate between “setup page” and “live data.”

async fn require_state(mode: &SharedAppMode) -> Result<Arc<AppState>, AppError> {
    let guard = mode.read().await;
    match &*guard {
        AppMode::Normal(s) => Ok(Arc::clone(s)),
        AppMode::Setup => Err(AppError::service_unavailable(
            "This endpoint is not available in setup mode",
        )),
    }
}

The database abstraction

Every engine hides behind the DatabasePool enum:

pub enum DatabasePool {
    Postgres(sqlx::PgPool, Option<crate::ssh::SshTunnel>),
}

The API never names sqlx. It calls pool.list_tables(), pool.get_columns(table), pool.query_rows(params). Each method is a match that dispatches to the module for that variant. Adding an engine means adding a variant and a module — no handler code changes. The mechanics are in Adding a database engine.

What is deliberately absent

  • No user accounts. SeeKi binds to localhost. If it can reach your machine, it is already inside the trust boundary.
  • No write path. There is no INSERT, UPDATE, or DELETE handler anywhere in src/api/. The only file the process ever writes is seeki.toml, and only from the setup flow.
  • No schema browser. The UI speaks spreadsheet, not database. Joins, foreign keys, and indexes are not surfaced.
  • No query editor. Filtering and sorting are parameter-driven; the string SELECT never reaches the user.

Where to go from here

  • Frontend map — the Svelte component tree and how state flows through it.
  • Adding a database engine — the checklist for shipping SQLite, MySQL, or anything else.
  • Build & release — how the frontend ends up inside the Rust binary, and how that binary reaches a GitHub release.