No description
  • Rust 57.7%
  • TypeScript 37.2%
  • PLpgSQL 2.9%
  • CSS 1.1%
  • Go Template 0.7%
  • Other 0.4%
Find a file
2026-07-18 12:39:32 +00:00
.cargo feat: migrate the job system from apalis to pg-jobs 2026-07-03 16:14:26 +02:00
.sqlx fix(db,server): policy RLS qualifiée et lookup d'attachment allégé 2026-07-18 12:45:53 +02:00
.woodpecker feat: enforce restricted RLS document access 2026-07-11 13:45:15 +02:00
apps fix(jobs): cap des SVG inline, warns corrélables et lock_duration explicite 2026-07-18 12:46:22 +02:00
charts/indara docs(chart): commentaires values à jour — owner aux migrations seules 2026-07-16 21:58:00 +02:00
crates test(jobs): cap streamé, svg imbriqué et commentaires 2026-07-18 12:46:45 +02:00
dev feat(worker): DATABASE_WORKER_URL, pool épinglé et garde de rôle au boot 2026-07-16 21:48:46 +02:00
docs fix(jobs): ExtractDocument en Detached — le réseau ne pinne plus la tx du job 2026-07-18 12:00:50 +02:00
frontend feat(frontend): badge et toggle Disabled dans l'admin utilisateurs 2026-07-17 03:21:37 +02:00
.dockerignore feat: docker image and helm chart 2026-06-10 15:13:39 +02:00
.gitignore chore: first pass ds migration 2026-07-10 12:13:09 +02:00
Cargo.lock feat(jobs): extraction des SVG inline en marqueurs infetchables 2026-07-18 10:40:14 +02:00
Cargo.toml refactor(server,db): groupes SCIM en écho sans persistance 2026-07-17 12:31:20 +02:00
compose.yaml fix: retours de revue finale — lifecycle compose, log drain, commentaires EN, doc chart 2026-07-12 17:06:20 +02:00
Dockerfile feat(docker): image runtime distroless cc-debian13:nonroot et toolchain 1.96 2026-07-16 19:18:09 +00:00
mise.toml chore(dev): make the dev server port configurable (default 3001) 2026-07-04 23:41:29 +02:00
README.md docs: aligne README et role_guard sur la frontière worker/owner 2026-07-16 22:11:44 +02:00

Indara

Personal web archive with BM25 full-text search, built on PostgreSQL + pg_textsearch. An alternative to Hister that keeps the search index in Postgres instead of an in-process bleve index.

Layout

  • apps/server — axum API + serves the frontend (/openapi.json for the OpenAPI spec; bring your own viewer)
  • apps/worker — pg-jobs worker running the fetch → extract → index pipeline
  • apps/migrate — applies application + pg-jobs migrations (k8s pre-upgrade job)
  • apps/cliindara-cli import <hister-dataset.json> through the HTTP API
  • crates/indara-core — config, language routing
  • crates/indara-db — queries + migrations/
  • crates/indara-jobs — pipeline steps, readability extraction, sanitization
  • frontend/ — React 19 + Vite SPA
  • charts/indara — Helm chart (OCI: oci://forge.netserv.fr/azsiaz/charts/indara)

Dev quickstart

docker compose up -d           # postgres (pg_textsearch preloaded) + dex + minio
cargo run -p indara-migrate
(cd frontend && npm ci && npm run build)
cargo run -p indara-server     # http://localhost:3001 (BIND_ADDR/PUBLIC_URL in .env.local)
cargo run -p indara-worker     # separate terminal

The server uses one restricted PostgreSQL connection through DATABASE_SERVER_URL. It authenticates as indara_control for identity/admin/jobs and executes SET LOCAL ROLE indara_runtime inside every RLS-protected document transaction. Only the migration binary uses the owner connection in DATABASE_URL. The worker connects through DATABASE_WORKER_URL, authenticating as indara_worker, a DML-only role with no membership; its boot guard refuses a superuser or owner credential. A new Compose volume creates both roles and their restricted membership automatically. In local development, use:

DATABASE_SERVER_URL=postgresql://indara_control@localhost:5433/app

For a volume created before that bootstrap existed, run the migration first, then normalize the development roles and membership:

cargo run -p indara-migrate
docker compose exec postgres psql -U postgres -d app \
  -c 'ALTER ROLE indara_control LOGIN NOINHERIT; ALTER ROLE indara_runtime NOLOGIN NOINHERIT; GRANT indara_runtime TO indara_control WITH ADMIN FALSE, INHERIT FALSE, SET TRUE'

Environment is loaded from .env.local via mise. Dev OIDC login (Dex): stef@example.com / password. Set OIDC_ADMIN_GROUP=indara-admins to make that development user an application administrator.

Raw page snapshots go to an S3-compatible bucket. The Compose stack runs MinIO (console at http://localhost:9001, indara / indara-dev-secret) and creates the indara-snapshots bucket on startup. Point the server and worker at it in .env.local:

INDARA_S3_ENDPOINT=http://localhost:9000
INDARA_S3_BUCKET=indara-snapshots
INDARA_S3_ACCESS_KEY_ID=indara
INDARA_S3_SECRET_ACCESS_KEY=indara-dev-secret

Import a Hister dataset with an API key created in the account UI (/me):

cargo run -p indara-cli -- import go-stdlib.json --api-key indara_...

Auth model

Every endpoint requires auth except /healthz, /api/auth/*, /opensearch.xml and the static assets. Browsers use the OIDC session cookie; third-party import tools send Authorization: Bearer <key> or X-API-Key. API keys have the fixed documents:write scope, expire after 30 days, and can only import documents plus read the key owner's basic /api/me identity and groups. Creating, listing and revoking keys requires a browser session.

OIDC profile pictures and group memberships are synchronized at login. OIDC_ADMIN_GROUP names the exact, case-sensitive OIDC group whose members are application administrators; an unset or empty value leaves the application without an administrator. Application admins can use the /api/admin/* identity, key and job views. They do not bypass document permissions.

An application admin may assign one current member as a group's admin, or leave the group without one. A group admin may delete a document assigned to that group or remove it from the group, but otherwise has ordinary user permissions.

Documents imported from the UI or API are private by default. A document is readable by its owner, by members of its owner group, or through an explicit user/group share; a share may expire or remain valid indefinitely. public means visible to every authenticated Indara user, not to anonymous Internet visitors. Only the individual owner can change visibility, assign a document to a group, or manage its shares. Raw fast-path HTML in document_sources is write-only to the HTTP server and is read only by the worker.

PostgreSQL RLS enforces these rules. The HTTP server opens one pool as indara_control, a direct LOGIN NOINHERIT role with identity/admin/jobs privileges and no direct document privilege. Its only membership is indara_runtime with ADMIN FALSE, INHERIT FALSE, SET TRUE. indara_runtime is a NOLOGIN NOINHERIT role whose document queries run only in short, role-local transactions carrying the current user id, authentication method and scopes. Startup validates both roles, their exact membership and their privilege boundaries. Only the migration binary receives the trusted owner DATABASE_URL; the HTTP server must never receive it. The worker connects through DATABASE_WORKER_URL as indara_worker, a DML-only role with no membership, and its boot guard refuses a superuser or owner credential.

Worker SSRF protection

The worker only fetches public destinations. Two layers:

  • Application (always on): fetch DNS resolution goes through INDARA_FETCH_DNS (default 1.1.1.1:53,9.9.9.9:53), any non-public IP (loopback, RFC1918, link-local, metadata, ULA…) is rejected, the resolved IP is pinned (anti-rebinding), and redirects are followed manually (INDARA_FETCH_MAX_REDIRECTS, default 5), revalidating each hop.
  • Network (K8s, backstop, opt-in): worker.networkPolicy.enabled=true and/or worker.ciliumNetworkPolicy.enabled=true. worker.fetchDns is the single source of truth: it feeds both the app's env and the egress DNS rules. Hardened mode: address the DB by IP/ExternalName and set worker.networkPolicy.allowClusterDns=false so the worker can only talk to public resolvers.

One partial BM25 index per language (documents_bm25_en, …). pg_textsearch has no per-row text config, so unknown languages are normalized to en at write time (v1 indexes everything as English; detection code exists behind DETECT_LANGUAGES). Queries must name the index via to_bm25query and repeat the partial-index predicate — see crates/indara-db/src/documents.rs.

Notes

  • .sqlx/ is committed; CI runs cargo sqlx prepare --check --workspace.
  • The migrate binary runs the application and pg-jobs migrators; the latter keeps its migration history in the jobs schema.
  • The CNPG cluster must preload pg_textsearch (postgresql.shared_preload_libraries) — see charts/indara/README.md.