This guide covers everything you need to run and deploy the Chitral platform locally and in production.
- Repository Structure
- Prerequisites
- Backend
- Frontend
- Running Both Together
- Contributing
- Architecture
- Testing
- Coding Conventions
- Environment Variables Reference
chitral/
├── backend/ # FastAPI backend (Python, SQLite, uv)
├── frontend/ # React + Vite + TypeScript frontend
└── spec/ # OpenSemantics Interface (OSI) spec files
| Tool | Version | Purpose |
|---|---|---|
| Python | ≥ 3.12 | Backend runtime |
| uv | latest | Python package & project manager |
| Node.js | ≥ 18 | Frontend runtime |
| npm | ≥ 9 | Frontend package manager |
Install uv (if not already installed):
curl -LsSf https://astral.sh/uv/install.sh | shThe backend is a FastAPI application with a SQLite database, managed with uv.
cd backend
# Install all dependencies (including optional connectors)
uv sync --extra connectors
# Or, for core dependencies only (no Snowflake connector):
uv syncThis creates a .venv virtual environment automatically.
cd backend
uv run run.pyThe server starts at http://localhost:8000 with hot-reload enabled.
Note: On first run,
chitral.db(SQLite) is created automatically in thebackend/directory.
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reloadIf you have an existing spec.yaml and need to migrate it into the database:
cd backend
uv run scripts/migrate.pyCreate a .env file in the backend/ directory to override defaults:
# backend/.env
DATABASE_URL=sqlite:///./chitral.db
API_PREFIX=/api
APP_NAME=Chitral
APP_VERSION=0.1.0
# Localhost frontends only by default. JSON array. Do not use ["*"] without auth.
CORS_ORIGINS=["http://localhost:5173","http://127.0.0.1:5173","http://localhost:3000","http://127.0.0.1:3000"]
# Optional: pin Fernet key for encrypting saved connector credentials.
# Generate: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# If unset, the backend creates backend/.datasource_key on first encrypt.
# DATASOURCE_ENCRYPTION_KEY=Security: Chitral has no API authentication. Saved data-source credentials are encrypted at rest (Fernet) in
chitral.db. ProtectDATASOURCE_ENCRYPTION_KEY/backend/.datasource_key. Use only on a trusted local machine. See the root README security section.If you don't set DATASOURCE_ENCRYPTION_KEY, a key is auto-generated at backend/.datasource_key. Saved datasource credentials are not portable across machines or fresh clones unless you pin that env var (or copy the key file along with the database).
For a Snowflake-connected workflow, add your Snowflake credentials:
SNOWFLAKE_ACCOUNT=your_account
SNOWFLAKE_USER=your_user
SNOWFLAKE_PASSWORD=your_password
SNOWFLAKE_WAREHOUSE=your_warehouse
SNOWFLAKE_DATABASE=your_database
SNOWFLAKE_SCHEMA=your_schemaOnce the backend is running, interactive docs are available at:
| Interface | URL |
|---|---|
| Swagger UI | http://localhost:8000/docs |
| ReDoc | http://localhost:8000/redoc |
| Health Check | http://localhost:8000/health |
| OSI YAML Export | http://localhost:8000/api/export/osi |
cd backend
uv run pytestTo run with verbose output:
uv run pytest -v# On the remote server
cd backend
uv sync --extra connectors
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4Use a process manager like systemd or supervisord to keep the process running.
Create a Dockerfile in backend/:
FROM python:3.12-slim
WORKDIR /app
# Install uv
RUN pip install uv
# Copy project files
COPY pyproject.toml uv.lock ./
RUN uv sync --extra connectors --no-dev
COPY . .
EXPOSE 8000
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]Build and run:
docker build -t chitral-backend ./backend
docker run -p 8000:8000 --env-file backend/.env chitral-backendPoint the platform to the backend/ directory and set the start command:
uv sync --extra connectors && uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
Set all required environment variables in the platform's dashboard.
Note on SQLite in production: SQLite is fine for small deployments. For concurrent multi-user production workloads, consider migrating to PostgreSQL by updating
DATABASE_URLto a Postgres connection string. No other code changes are required — SQLAlchemy handles the rest.
The frontend is a React 19 application built with Vite, TypeScript, and Tailwind CSS v4.
cd frontend
npm installcd frontend
npm run devThe app starts at http://localhost:5173 by default. It expects the backend API to be running at http://localhost:8000.
From the repo root:
make lint # backend (ruff) + frontend (eslint)
make lint-backend # ruff only
cd frontend && npm run lint # eslint onlyBackend lint/format:
cd backend
uv run ruff check app tests
uv run ruff format app testscd frontend
npm run test:unitUses Vitest for fast component/hook tests (e.g. src/hooks/*.test.ts).
Compile TypeScript and bundle for production:
cd frontend
npm run buildOutput is written to frontend/dist/. Preview the production build locally:
npm run preview- Build the project:
cd frontend && npm run build
- Deploy the
frontend/dist/directory to your static host. - Set the environment variable
VITE_API_URL(if used) to point at your deployed backend URL.
For Vercel, add a vercel.json at the repo root to handle SPA routing:
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}# frontend/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80Build and run:
docker build -t chitral-frontend ./frontend
docker run -p 80:80 chitral-frontendSet the build command and publish directory:
| Setting | Value |
|---|---|
| Build command | npm run build |
| Publish directory | dist |
For local full-stack development, run the backend and frontend in separate terminals:
Terminal 1 — Backend:
cd backend && uv run run.pyTerminal 2 — Frontend:
cd frontend && npm run devThe frontend calls the backend API directly at http://localhost:8000 by default (override with VITE_API_URL in frontend/.env). Ensure the backend is running and that CORS_ORIGINS includes your frontend origin.
Chitral is open source under the MIT License.
- Fork the repository and create a feature branch from
main(e.g.feat/postgres-sinkorfix/import-warnings). - Keep changes focused — one logical change per PR when possible.
- Run the full check suite locally before opening a PR (see Testing and
make cibelow). - Open a pull request with:
- A short summary of why the change is needed
- A test plan checklist (what you ran, what you could not run)
- Expect review feedback on correctness, tests, and consistency with existing patterns. We do not require a CLA.
- Tests for behavior changes (backend pytest and/or frontend Vitest/Playwright as appropriate)
- No committed secrets (
.env,backend/.datasource_key, credentials) - Google-style docstrings on new public Python APIs
- Ruff-clean backend code (
make lint-backend)
Before changing backend behavior, read backend/ARCHITECTURE.md — it explains the layered layout (api/ → crud/ → models/, connector ABCs, generation engine) and common extension points.
For connector design history and Snowflake-specific details, see also spec/connectors.md.
- Implement the ABCs in
backend/app/connectors/<name>/:client.py—BaseConnector(schema introspection,test_connection)profiler.py— extendsBaseProfiler(aggregate SQL profiling)sampler.py— row sampling for JSON/LLM few-shot (SQL connectors can extendSqlTableSamplerinconnectors/sql_sampler.py)inferrer.py—BaseRelationshipInferrer(FK metadata inferrers can extendInformationSchemaFkInferrerinconnectors/fk_inferrer.py)
- Add Pydantic credential/request schemas in
backend/app/schemas/connector.py. - Register API routes in
backend/app/api/connectors.py(/test,/introspect,/infer-relationships,/import). - Wire saved sources — extend
build_credentials/ dispatch inbackend/app/api/datasources.pyif the connector supports predefined connections. - Frontend — add a form tab in
ConnectorsSidebar, hooks inuseSnowflakeIntrospect.ts(or a sibling hook), and types infrontend/src/types/api.ts. - Tests — unit tests under
backend/tests/connectors/; Docker integration tests viamake test-db-upwhen the warehouse has a container recipe indocker-compose.test.yml.
These are known duplication hotspots — safe to leave as-is for now:
backend/app/api/connectors.py— repeated per-connector route handlers (/test,/introspect,/import) could become a generic registry; deferred to avoid a large risky refactor before OSS.- MongoDB profiler — shares patterns with SQL profilers but document-model stats differ enough that a shared base has not been extracted yet.
make test
# or
cd backend && uv run pytest -vcd frontend && npm run test:unitPlaywright auto-starts backend + frontend dev servers (see frontend/playwright.config.ts).
Marketing seed database (required): Several specs (datasources.spec.ts, synthesis-panel.spec.ts, and others) read data/marketing_seed.db, which is gitignored. Generate it once before running E2E:
cd backend
uv run python scripts/seed_marketing.py --source-onlyThis writes data/marketing_seed.db at the repo root. Omit --source-only if you also want the script to reset canvas metadata in the app DB.
make test:e2e
# or
cd frontend && npx playwright testHeaded mode for debugging: npx playwright test --headed
Shared helpers live in frontend/e2e/helpers.ts. Core specs: canvas.spec.ts, context-menu.spec.ts.
Postgres, MySQL, and MongoDB integration tests require local containers:
make test-db-up # Postgres :5433, MySQL :3307, MongoDB :27018
make test-postgres # or test-mysql / test-mongodb / test-*-profiler / test-*-api
make test-db-downWithout these services, make test skips ~88 integration-marked tests by design.
make ciRuns, in order: make install (backend + frontend deps) → make test (backend pytest) → make lint (ruff + eslint) → make build (frontend production build).
Use Google-style docstrings on public classes and functions in backend/app/:
def sample_table(self, database: str, schema: str, table: str) -> TableSample:
"""Fetch random rows from a single table.
Args:
database: Catalog/database name.
schema: Schema namespace.
table: Table to sample.
Returns:
Sampled rows packaged as a ``TableSample``.
Raises:
ConnectorError: When the table is empty or sampling fails.
"""Configuration lives in backend/pyproject.toml under [tool.ruff]. Before committing backend changes:
cd backend
uv run ruff check app tests --fix
uv run ruff format app testsOr from the repo root: make lint-backend.
ESLint 9 flat config in frontend/eslint.config.js. Run npm run lint or make lint.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
sqlite:///./chitral.db |
SQLAlchemy database URL |
API_PREFIX |
/api |
URL prefix for all API routes |
APP_NAME |
Chitral |
App name shown in docs |
APP_VERSION |
0.1.0 |
App version shown in docs |
CORS_ORIGINS |
localhost Vite/CRA origins (JSON array) | Allowed browser origins. Override for other hosts; never use ["*"] without auth. |
DATASOURCE_ENCRYPTION_KEY |
— | Optional Fernet key (url-safe base64) for encrypting saved data-source credentials at rest. Generate: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())". If unset, the backend writes backend/.datasource_key on first encrypt. |
MONGODB_URL |
— | Optional dev URI; prevents pydantic-settings from rejecting the env var. Connector requests still pass credentials explicitly. |
OPENAI_API_KEY |
— | Required for LLM-assisted JSON/VARIANT column synthesis |
OPENAI_MODEL |
gpt-4o-mini |
OpenAI model name for JSON synthesis |
SNOWFLAKE_ACCOUNT |
— | Snowflake account identifier |
SNOWFLAKE_USER |
— | Snowflake username |
SNOWFLAKE_PASSWORD |
— | Snowflake password |
SNOWFLAKE_WAREHOUSE |
— | Snowflake warehouse name |
SNOWFLAKE_DATABASE |
— | Snowflake database name |
SNOWFLAKE_SCHEMA |
— | Snowflake schema name |
| Variable | Default | Description |
|---|---|---|
VITE_API_URL |
http://localhost:8000 |
Base URL for the backend API |