Skip to content

Architecture

The Organization Manager sits between the writers of allocation data (the Django admin portal, customer integrations posting to the REST API) and the IP Mapper that the rest of Platform Filter reads from at query time. PostgreSQL is the source of truth shared between the admin portal and orgmanager; NATS is the transport that carries session updates downstream to the IP Mapper.

Role in Platform Filter

flowchart LR
    A[Admin portal / API clients] -- REST --> O[orgmanager]
    O <-- SQL --> P[(PostgreSQL)]
    O -- NATS --> M[ipmapper]
    M -- query path --> R[Platform Filter]
  • Upstream writers. The Django admin portal in admin-portal writes the same PostgreSQL tables directly. Customer integrations drive the REST API. Both paths bump org_version on every change.
  • PostgreSQL. Holds organisations, prefixes, and overrides plus their org_version row used by the version scanner. SQLite is available in dev and test builds but is not shipped in the production RPM (see Storage backends).
  • NATS to IP Mapper. Orgmanager publishes session updates and cleanup requests on subjects under the configured itsy prefix, targeting the ipmapper service.

Core concepts

These terms appear throughout the configuration, REST API, and operations pages. This is the single source of truth for them.

Organisation

A tenant. Identified by an opaque org_id string in the URL path and on the wire. CIDRs are isolated per organisation — two organisations may have overlapping or identical CIDRs in their own namespaces without conflict, but two organisations may not own the same prefix in the global allocation table.

Prefix

An IP CIDR block that an organisation has been allocated. Prefixes are added and removed via PATCH /org/{org}/prefixes. Cross-org overlap is refused.

Override

A specific CIDR rule with attributes — username, location, deviceid, optional opaque meta. Overrides live within (or on top of) the organisation's prefixes. An override whose CIDR is not yet covered by any prefix is stored but marked outside-prefixes and held in reserve. When a covering prefix is added later, the override becomes active.

Effective range

The post-collapse, non-overlapping output the IP Mapper sees. Overrides are sorted most-specific-first and laid down on top of the prefix tree. Where an override is split by a more specific one, the original CIDR's attributes still apply to the unsplit pieces. There is no attribute inheritance — each range carries the attributes of exactly one source override.

Worked example. Three nested overrides:

10.0.0.0/8      → username: "org-default"
10.1.0.0/16     → username: "department-x", location: "west"
10.1.2.0/24     → username: "team-y"

Collapsed effective ranges:

Range username location
10.0.0.0 – 10.0.255.255 org-default (none)
10.1.0.0 – 10.1.1.255 department-x west
10.1.2.0 – 10.1.2.255 team-y (none)
10.1.3.0 – 10.1.255.255 department-x west
10.2.0.0 – 10.255.255.255 org-default (none)

team-y does not inherit location: west.

Session

One unique combination of (org, CIDR, username, location, deviceid). Sessions are what orgmanager publishes to the IP Mapper. The session ID is deterministic in those five inputs — a change to any attribute yields a new session, which the IP Mapper sees as a stop of the old plus a start of the new.

Revision

A monotonic per-organisation counter attached to session start / update actions. The IP Mapper uses it to clean up sessions that predate a full redeploy or a prefix removal — see Cleanup contract with the IP Mapper.

active vs outside-prefixes

Computed at runtime by checking whether an override's CIDR is fully contained within the union of the organisation's current prefixes. The status is not stored. The API recomputes it on every read, so adding or removing a prefix is reflected in _status on the next read of any covered override; the IP Mapper-side application of that flip happens on the next deploy.

PostgreSQL concurrency model

Orgmanager protects writes at three layers.

Per-organisation in-process mutex

Every write path takes a per-org_id mutex inside the orgmanager process. Concurrent writes to the same organisation serialise; writes to different organisations run in parallel.

Single-organisation transaction with org_version bump

Each write runs inside one PostgreSQL transaction that ends with an increment of the organisation's org_version. The version scanner relies on this — a bumped org_version is the signal that a redeploy is needed, whether the change came from this orgmanager, another orgmanager, or the Django admin portal.

PostgreSQL advisory lock for prefix overlap

Prefix mutations additionally take a transaction-scoped PostgreSQL advisory lock before checking for overlap with other organisations. Without the lock, two prefix-add transactions running under READ COMMITTED could each see no overlap and both commit, leaving two organisations claiming the same address space.

Operator implication: prefix mutations serialise across the cluster (a single global lock); everyday override writes serialise per organisation but run in parallel across organisations.

Async deployment to the IP Mapper

Writes commit to PostgreSQL synchronously and return to the API client immediately. Publishing the resulting sessions to the IP Mapper happens asynchronously on a worker pool.

Worker pool and coalescing

Each write enqueues the affected org_id for deployment. Workers pull from the queue, load the organisation's state, compute effective ranges, group them into sessions, diff against the last-deployed state, and publish the result.

Multiple writes that arrive for the same organisation while a deploy is already running coalesce into one follow-up deploy. Bursts of writes therefore produce one published diff, not one per write.

Three deploy kinds

  • Normal (diff). The default. Load the organisation, compute current sessions, diff against the previously deployed snapshot, and publish only the differences as start / update / stop actions. Sessions whose allocations have not changed are not sent.
  • Full redeploy. Re-publish every current session under a fresh per-organisation revision, then ask the IP Mapper to clean any session for this organisation whose revision is older than the one we just wrote. Triggered by POST /org/{org}/redeploy and by the optional periodic full-redeploy timer.
  • Delete. Stop every deployed session for the organisation and ask the IP Mapper to clean the organisation's namespace subject-wide. Triggered by DELETE /org/{org}?confirm=true and by the version scanner when an organisation disappears from PostgreSQL.

Retries

Each deploy attempt that fails to publish to the IP Mapper retries with a one-second sleep, up to a fixed budget. After the budget is exhausted the failure is logged and the worker moves on; the next write or the next periodic full-redeploy will pick the organisation back up.

Cleanup contract with the IP Mapper

Most session changes ride on start / update / stop actions in the diff. The separate cleanup command runs in four cases:

  • Full redeploy. After publishing every current session under a fresh revision, orgmanager calls the IP Mapper cleanup command bounded by revision_lt, asking it to drop any session for this organisation that predates the redeploy. This is what makes a full redeploy self-healing without a separate "stop everything" step.
  • First deploy of an organisation in this orgmanager process. After a restart, an orgmanager has no in-memory record of which sessions the IP Mapper currently has for an organisation. The first deploy after the process started — including the deploys the startup version scan queues — therefore runs an unbounded-by-subnet revision cleanup against the IP Mapper, so any sessions left over from a previous orgmanager process are trimmed.
  • Prefix removal. After a successful diff publish that included prefix removals, orgmanager calls cleanup bounded by both the organisation and the removed subnets, so unrelated sessions in other prefixes are left untouched.
  • Override deletion. Override deletes pass the deleted CIDRs as the cleanup subnet list, and the diff publish that follows ends with a cleanup call bounded by the organisation plus those subnets. The intent is the same as for prefix removal — keep cleanup scoped tightly enough that unrelated sessions stay put.

A cleanup failure is treated as a deploy failure: the deploy attempt errors out, the worker retries (1 s sleeps, up to the retry budget), and only after the retry budget runs out does the deploy land in the error log and bump orgmanager_deploy_errors_total. Even then the next full-redeploy retries the cleanup, so transient IP Mapper unavailability heals on its own.

Periodic background work

Two timers run in the background, both controlled by the deploy section.

Version scan

Walks every organisation row in PostgreSQL and compares its org_version against the last deployed version held in memory. The scan picks up:

  • Driftorg_version increased since the last deploy. Queues a normal redeploy.
  • Discovery — an organisation in PostgreSQL that the orgmanager has not seen before (typically created via the Django admin portal). Queues a deploy.
  • Disappearance — an organisation in memory that no longer exists in PostgreSQL. Queues a delete.

Setting version_check_interval: 0 disables the scan. The first scan also runs at startup to discover existing organisations, and runs even with the interval set to 0 — that initial scan is what populates the in-memory cache after a restart.

Periodic full redeploy

If enabled, periodically queues a full redeploy for every cached organisation. This re-asserts every session at the IP Mapper and trims older revisions, healing any state the IP Mapper may have missed. A typical operator setting is hours, not minutes — the diffing path already keeps the IP Mapper in sync, this exists as a bounded safety net. full_redeploy_interval: 0 disables it.

Storage backends

  • PostgreSQL is the production target. The DSN is the only database configuration knob; it accepts the standard postgres:// URL form.
  • SQLite exists for dev and tests and is selected by prefixing the DSN with sqlite:. The production RPM is built without CGO and does not link the SQLite driver — pointing database.dsn at a sqlite: URL on a deployed box will refuse to start with a message that names the missing CGO driver. See Configuration → database for the matching DSN reference.