Skip to content

Running the app

The web UI is part of the platform: a FastAPI backend, a Postgres database, a scheduler, and the Nuxt single-page app served as static assets. The core framework does not need any of it; the platform needs the core.

Packages

Package Role
interloper-db Persistence and migrations.
interloper-api The HTTP backend the UI talks to.
interloper-scheduler Cron controller, hook evaluator, credential renewal, queue worker, reaper, launchers.
interloper-app The built SPA, served by the API process.

Images

Every release publishes one image per role to ghcr.io/digitl-cloud, plus a Helm chart that deploys them. Each image comes in two variants:

Tag Carries
interloper-<role>:<version> Loaded: the role's own packages, every component class a catalog can name (ready-made sources, BigQuery and GCS destinations, the Slack hook) and what the role runs on (both launchers on the scheduler, the agent on the api, telemetry).
interloper-<role>:<version>-slim Slim: interloper-core plus the role's own packages, nothing optional. No sources, no destinations, no launcher beyond in-process, no telemetry SDK.

The roles are api, scheduler, core, mcp and frontend (nginx, single variant). The loaded images are what the Helm chart deploys and what a deployment configured from the catalog wants: launcher.type and agent.enabled select between things already in the image, so switching one is a restart and never a rebuild.

core is the framework itself, with no platform around it: the Kubernetes runner uses it as the per-asset Job target, and interloper-core:<version>-slim is interloper-core and nothing else, which makes it the base to build on when you ship your own components.

Two rules decide what a loaded image carries. Any role that builds a catalog must be able to import every class the catalog can name, which is why the api and mcp ship sources, destinations and hooks they never execute: they describe them, and a class that fails to import is skipped with a warning rather than an error, so the answer would quietly be wrong. The vendor SDKs (bingads, facebook-business, google-ads) are the exception, because the asset modules guard those imports: only the roles that actually execute assets, scheduler and core, carry them. That single distinction is why the api and mcp images are a fraction of the size of the two that run assets.

Extending an image

The slim images exist to be built on. Every package is on PyPI, so adding what a deployment needs is an install in a child image:

FROM ghcr.io/digitl-cloud/interloper-scheduler:<version>-slim

USER root
RUN pip --python /interloper/.venv/bin/python install --no-cache-dir \
      "interloper-assets[google]" \
      interloper-google-cloud \
      my-own-connectors
USER app

The --python flag matters, and it goes before the subcommand. The application lives in the virtualenv at /interloper/.venv, which was built by uv and therefore has no pip of its own; a bare pip install would resolve to the base image's interpreter instead and land the package somewhere the app never looks, with no error to show for it.

Anything installed this way registers itself: a package declaring components under the interloper.components entry point joins the catalog on the next start, with no change to the image's command. List what you want enabled under catalog and the deployment sees exactly that, as described in Catalog and Entry points & registries.

The same recipe works on a loaded image when you want everything it ships plus your own components.

One process per role

Each role starts through the command its package contributes, which is what the images run:

interloper api --no-create-tables                                  # the api image: the HTTP API
interloper scheduler --cron --worker --reaper --no-create-tables   # the scheduler image: cron (with hooks and renewal), worker, reaper

interloper scheduler takes --no-cron, --no-worker and --no-reaper to split its services across replicas: cron and the reaper are singletons, the worker scales out, and replicas split the work through row locks. Without a flag, a service follows its enabled setting (cron.enabled, worker.enabled, reaper.enabled), which every pod reading the same interloper.yaml shares; that is why the images and the chart name all three.

One process

interloper app --api --cron --worker --reaper

runs every service in one process against the configured Postgres, serving the UI on port 3000. It needs interloper-app installed. Each service can be toggled off on its own, and --dev runs the Nuxt dev server with hot reload instead of the built assets.

Configuration

interloper.yaml in the working directory, or INTERLOPER_* variables:

postgres:
  host: localhost
  user: interloper
  database: interloper          # password: INTERLOPER_POSTGRES_PASSWORD

auth:
  google_client_id: ...
  google_client_secret: ...
  google_redirect_uri: https://app.example.com/api/auth/google/callback
  allowed_domains: [example.com]

catalog:
  - my_package.sources.Shop
  - interloper_assets.facebook_ads.connector.source.FacebookAds

The file is not interpolated: a field left out of a block is read from its INTERLOPER_<SECTION>_<FIELD> variable, which is where secrets go. INTERLOPER_ENCRYPTION_KEY is required: resource configs are encrypted at rest. Login is Google OAuth. allowed_domains restricts who may sign up; super_admin_emails bootstraps platform administrators, promoted by interloper db init and at login (promote-only). Once one exists, super-admins grant and revoke the access from the admin portal's Users page. In-house OAuth app credentials for connectors live in INTERLOPER_<PROVIDER>_CLIENT_ID, _CLIENT_SECRET and _REDIRECT_URI; a provider without them has no sign-in tab, only manual credential entry. The full settings list is in Settings.

Database

interloper db init          # create the database, tables and run migrations; idempotent
interloper db upgrade       # migrate an existing database to head

The serving commands (interloper api, interloper scheduler, interloper app) also create missing tables on startup unless --no-create-tables is passed.

Local development

The repository ships a dev harness: make dev provisions a local Postgres database, seeds a super-admin, an organisation, the demo source and a daily job, and starts every service with the Nuxt dev server on port 3000. make compose-up does the same in Docker. The screenshots on the Tour come from that harness with a few more demo components added.