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
- A browser asks for
/. The binary answers with the embeddedindex.html. - The UI asks for
/api/tables. The binary answers out of the database pool. - There is no step three.
Request flow
src/main.rssrc/api/mod.rssrc/db/mod.rssrc/db/postgres.rs/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 return503.
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.
| Path | Purpose |
|---|---|
src/main.rs | Boots tracing, loads the config, picks a mode, builds the router, binds a TCP listener. |
src/config.rs | The typed shape of seeki.toml: AppConfig, DatabaseConfig, DatabaseKind, SshConfig, display overrides, secrets. |
src/app_mode.rs | The AppMode enum (Normal / Setup) and the SharedAppMode handle everything else holds. |
src/api/mod.rs | Router wiring and the read-only handlers: tables, columns, rows, display config, CSV export, status. |
src/api/setup.rs | First-run endpoints: test-connection and save. The only code path that writes seeki.toml. |
src/db/mod.rs | The DatabasePool enum — the abstraction every engine implements. Shared types: TableInfo, ColumnInfo, QueryResult, ValidationError. |
src/db/postgres.rs | The PostgreSQL engine. List tables, describe columns, page rows, stream CSV. Identifier-quoting lives here. |
src/ssh/mod.rs | The 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.rs | The rust-embed asset handler. Everything not under /api/* ends up here. |
src/auth/mod.rs | Localhost-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, orDELETEhandler anywhere insrc/api/. The only file the process ever writes isseeki.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
SELECTnever 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.