Architecture
How Fabrik is put together — the services, data stores, queues, and WebSocket channels behind the visual query builder.
Fabrik runs as a stack of seven containers: one Django ASGI web process, a Celery worker and scheduler, a React frontend served by Nginx, and three backing services (PostgreSQL, Neo4j, Redis). Nothing in the stack is a single point of unique magic — it's a conventional Django + Celery + Channels application with one twist: Neo4j stores the ACI Managed Information Model so the query builder can reason about class relationships at draw time.
The web tier is stateless. Every long-running thing — query execution, AWX job dispatch, notification delivery, MIM import — is a Celery task. Real-time updates travel over WebSockets through Django Channels, with Redis as the channel layer. AWX status updates arrive through a Django webhook endpoint plus a 30-second beat task that polls AWX as a safety net.
High-level diagram
The stack at a glance
| Layer | Technology |
|---|---|
| Frontend | React 19, Vite, React Flow, Zustand, TanStack Query, Tailwind (served by Nginx in production) |
| Backend | Django 6 + Django REST Framework + Channels, served by Daphne (ASGI) |
| Workers | Celery (worker + beat) |
| Relational store | PostgreSQL 17 |
| Graph store | Neo4j 5.26 (ACI MIM metadata only) |
| Cache, Celery broker, channel layer | Redis 8 |
| Auth | JWT for API clients; optional LDAP / Active Directory |
| Secrets at rest | Fernet symmetric encryption for APIC passwords, AWX OAuth tokens, and other credentials |
Services
Seven containers make up a default deployment:
| Container | Purpose |
|---|---|
postgres | Primary relational store — users, saved queries, scheduled tasks, AWX metadata, Time Machine snapshots, audit logs. |
redis | Celery broker, Celery result backend, Django Channels channel layer, and general application cache. |
neo4j | Stores the ACI Managed Information Model — class hierarchy, relationships, and property metadata. |
backend | The ASGI web process. Entrypoint runs database migrations, bootstraps the MIM cache, then starts Daphne on port 8000. Serves REST, WebSocket, and the AWX webhook receiver. |
celery-worker | Executes background tasks across seven queues (see below). |
celery-beat | Periodic scheduler — fires the recurring tasks listed in the next section. |
frontend | In production, Nginx serves the compiled Vite bundle and proxies /api and /ws to the backend. |
Data stores
PostgreSQL is the system of record. Every durable object — user accounts, saved queries, scheduled tasks, AWX requests and executions, Time Machine snapshots, audit events — lives here.
Neo4j stores only the ACI MIM metadata. Nodes are Class and Property; edges describe containment, relative-name mappings, and property ownership. The query builder walks this graph to validate connections on the canvas and suggest what classes a given parent can contain.
Neo4j does not mirror your live fabric. It never holds tenant, EPG, or endpoint data. Every live query goes straight to the APIC over REST. Neo4j exists so the canvas knows that fvBD can contain fvSubnet — not what subnets exist on your BDs right now.
Redis plays four roles: it's the Celery broker (DB 0), the Celery result backend (DB 1), the Django Channels channel layer (so a Celery worker can push a WebSocket message to a browser), and a general cache — including the TTL'd MIM query cache (DB 2).
Celery queues
The worker listens on seven queues. Each has a narrow purpose so a long AWX run can't starve user-triggered query execution, and vice versa.
| Queue | What runs here |
|---|---|
query_exec | On-demand APIC query executions triggered by the user |
scheduled | Scheduled task runner and the tasks it dispatches |
awx_monitor | Lightweight AWX job polling (awx.sync_running_jobs) |
awx_exec | AWX automation request execution — can be heavy |
mim_import | MIM registry imports (mim_registry.import_mim_version) |
maintenance | Daily and periodic housekeeping — notification cleanup, stale-execution sweep, password-reset code expiry |
celery | Default queue, used as a fallback |
Celery beat schedule
Celery beat fires seven recurring tasks:
| Task | Schedule |
|---|---|
queries.check_scheduled_tasks | Every 1 minute |
awx.sync_running_jobs | Every 30 seconds |
awx.cleanup_stale_executions | Every 30 minutes |
notifications.flush_notification_digests | Every 60 seconds |
notifications.check_escalations | Every 5 minutes |
notifications.cleanup_old_notifications | Daily at 04:00 |
users.cleanup_expired_reset_codes | Daily at 05:00 |
All schedules are expressed in the server's timezone, controlled by the TZ environment variable.
How AWX status updates arrive
Fabrik supports two paths for AWX job status, both feeding the same JobMonitor reconciler:
- Webhook path. AWX is configured to POST job and workflow status notifications to
https://<fabrik>/api/awx/webhooks/receiver/. The receiver validates the HMAC signature and callsJobMonitor.sync_job_status(execution_id)synchronously. This is the fast path — updates arrive within a second of AWX firing them. - Poll path. Every 30 seconds,
awx.sync_running_jobs(Celery beat) calls AWX for every non-terminal execution and reconciles the same way. This catches anything the webhook missed — AWX outage, dropped connection, signature mismatch.
Either way, JobMonitor writes the new status to Postgres and emits a WebSocket event through Channels so the browser detail page updates in real time. Live job output is streamed by a separate per-execution Celery task (awx.stream_job_output) that polls AWX's job_events API at sub-second intervals and pushes each chunk over WebSocket while persisting it to JobOutputChunk for later replay.
WebSocket channels
Daphne terminates WebSocket connections at five paths:
| Path | Purpose |
|---|---|
ws/chain-execution/<job_id>/ | Progress and results for a running query chain |
ws/notifications/ | Per-user real-time notifications |
ws/awx/request/<request_id>/ | Aggregate status for a multi-execution AWX request |
ws/awx/execution/<execution_id>/ | A single AWX execution: status, output stream, final result |
ws/mim-import/<task_id>/ | Progress while importing a new MIM version from the registry |
Authentication is checked by middleware before any consumer code runs; the frontend passes a short-lived JWT ticket at connection time.
External systems
Fabrik relies on a small number of systems that live outside the stack. None of them is a container Fabrik ships.
- Cisco APIC — the required one. Every live query goes here. An administrator registers each APIC as a connection; credentials are stored Fernet-encrypted.
- AWX / Ansible Tower — optional. Needed only if you use the Ansible module. Fabrik authenticates with an OAuth token, posts launch requests, and receives status updates via webhook plus the 30-second poll.
- SMTP server — optional. Used for notification emails. Bring your own relay; Fabrik does not run one.
- LDAP / Active Directory — optional. The backend image includes the LDAP libraries so you can point Fabrik at a corporate directory for sign-in.
AWX projects may themselves be backed by a Git repository — GitLab, Gitea, GitHub Enterprise, or any other — so AWX can pull playbooks from version control. That's configured entirely on the AWX side. Fabrik has no SCM integration of its own and does not read or write to any Git service. If you don't use Ansible, you don't need an SCM at all.
Request lifecycle
It helps to trace one request end-to-end. Suppose you draw a query on the canvas and click Execute:
- The frontend POSTs the query graph to the backend REST API.
- The backend validates it, creates a
ChainExecutionrecord, and enqueues a task on thequery_execCelery queue via Redis. - A Celery worker picks up the task, resolves the APIC connection, and calls the APIC REST API — one request per node in the chain, possibly in parallel.
- As each node completes, the worker pushes a progress event through the Channels layer (backed by Redis) to the group
chain-execution-<job_id>. - Your browser, already connected to
ws/chain-execution/<job_id>/, receives each event and updates the canvas in real time. - When the final node finishes, the worker persists the result to Postgres, emits one last WebSocket event, and the task ends.
The AWX path looks the same up to step 3, with two differences: the task runs on the awx_exec queue and calls AWX instead of the APIC; subsequent status updates arrive through the webhook receiver (and the 30-second beat poll as a fallback) rather than being emitted by the worker itself.
Deployment topology
A default install runs all seven containers on a single Docker host. The frontend is the only container you typically expose publicly; everything else binds to the internal Docker network. A reverse proxy (Nginx, Caddy, HAProxy — your choice) sits in front to terminate TLS, route / to the frontend, and forward /api and /ws to the backend.
Horizontal scaling is possible — multiple Celery workers, multiple backends behind a load balancer — but the defaults are sized for a single-host deployment. For install, environment variables, TLS, backups, and upgrades, see the Deployment guide.