Registrar, ThingMake & Arsenal — Design Document

This document describes the design for the Registrar entity model, the ThingMake manufacturer console, the Arsenal platform-admin console, the provisioning workflow, saved filters, and cross-account registrar access control.

Overview

The base ThingHub model stored registrar_id as an account ID string directly on each Thing. This design introduces Registrar as a first-class entity with a platform-admin-approved lifecycle, enabling:

  1. ThingMake — a dedicated manufacturer console for enrolling things and managing registrars

  2. Arsenal — a platform admin console for approving registrar creation and access requests

  3. Provisioning — a distinct step after enrollment that generates access/join codes and QR URLs

  4. Saved filters — named filters across accessible registrars for discovering and bulk-adding things

  5. Cross-account registrar access — platform-admin-gated access for browsing registrars and OTA target filters

  6. ThingHub refocus — repository and scope management only; enrollment moves to ThingMake

Data model

Registrar

A Registrar is a named container of things, owned by a manufacturer account and approved by a platform admin.

Registrar
├── id              (UUID, primary key)
├── name            (display name, e.g. "RB-200X Production Line")
├── account_id      (manufacturer account that owns this registrar)
├── status          (PENDING → ACTIVE | REJECTED)
├── created_at
└── updated_at

Rules:

  • A manufacturer can have multiple registrars (one per product line, factory, etc.)

  • A thing belongs to exactly one registrar (immutable after enrollment)

  • Registrar creation requires platform admin approval via Arsenal

  • The manufacturer gets default access to their own registrar upon approval

Registrar Access Request

A RegistrarAccessRequest represents a tenant account requesting blanket access to all things in a registrar.

RegistrarAccessRequest
├── id              (UUID, primary key)
├── registrar_id    (FK → Registrar)
├── account_id      (tenant requesting access)
├── status          (PENDING → APPROVED | REFUSED)
├── created_at
└── updated_at

Rules:

  • Access is blanket — grants visibility to all things in the registrar

  • Approval is a platform admin decision (via Arsenal), not the manufacturer’s

  • One active request per (registrar, account) pair

  • Currently used for OTA target-filter access; reusable for future capabilities

Saved Filter

A RegistrarSavedFilter is a named, persisted search across one or more accessible registrars.

RegistrarSavedFilter
├── id              (UUID, primary key)
├── account_id      (who saved it)
├── name            (e.g. "CJ-5000 Warehouse Robots")
├── registrar_ids   (comma-separated registrar IDs to search across)
├── filter          (attribute filter conditions, e.g. "model=CJ-5000&location=warehouse-b")
├── created_at
└── updated_at

Rules:

  • Saved filters execute live queries against registrar things (not snapshots)

  • Can reference multiple registrars — results are merged

  • Registrar access is validated at execution time (revoked access = execution fails)

  • Newly enrolled things matching the filter appear on re-execution

  • Dismissed things disappear on re-execution

  • Used to discover things, then bulk-add to repository via POST /api/v1/repository/things

Thing lifecycle changes

Things gain a join_code (NanoID) and provisioned_at timestamp. The lifecycle becomes:

ENROLLED (draft)
  │  Thing created in ThingMake, assigned to a registrar.
  │  No join code yet. Cannot be joined.
  │
  ▼
PROVISIONED
  │  Provisioning triggered (manually, via API, or from a device).
  │  join_code (NanoID) generated. QR code URL available.
  │  Serial number optional — things can be provisioned without SN.
  │
  ▼
JOINED (in repository)
     User scans QR or enters join code → thing linked to account's repository.

New fields on Thing:

Thing (additions)
├── join_code         (NanoID, unique, generated at provisioning — NULL until provisioned)
├── provisioned_at    (timestamp, NULL until provisioned)
└── registrar_id      (now references Registrar.id instead of raw account ID)

The join_code is a URL-safe NanoID (21 chars). The QR code URL is computed:

https://console.flexgalaxy.ai/thinghub/join/{join_code}

This deep-links into ThingHub’s join flow.

The existing access_code (XXXX-YYYY-ZZZZ format) remains for manual entry. Both access_code and join_code can be used to join a thing.

Repository model

Each tenant account has one thing repository — the collection of all things joined by that account.

Tenant Account
  └── Thing Repository (single, account-scoped)
        ├── Thing A (from Registrar X, joined via QR scan)
        ├── Thing B (from Registrar X, joined via QR scan)
        └── Thing C (from Registrar Y, joined via batch URL)

The repository is represented by the set of active ThingAssignment records for the account. No new entity is needed. See ADR-0003 for how the repository scope is materialized in the schema.

Application architecture

New apps

App

URL

Users

KC Client

Realm

ThingMake

console.flexgalaxy.ai/thingmake/

Manufacturers

thingmake

flexgalaxy (tenant)

Arsenal

stargate.flexgalaxy.ai/arsenal

Platform admins

arsenal

master

Modified apps

App

Changes

Why

ThingHub

Remove enroll and join buttons. Add repository listing (/api/v1/repository/things), scope things management, registrar browser with saved filters.

ThingHub becomes the repository and scope management app. Enrollment moves to ThingMake. Things enter the repository via access_code or registrar bulk add.

OTA target filters

Show accessible registrars. Filter things by registrar for OTA rollout targeting.

OTA target filters require registrar access (cross-account authorization).

ThingMake

Purpose: Manufacturer console for enrolling things, provisioning, and managing registrars.

Location:  console.flexgalaxy.ai/thingmake/
Backend:   ThingHub API (same backend, proxied via console-gateway)
KC Client: thingmake (flexgalaxy realm)
Port:      5189 (local dev)

Pages:

Page

Route

Description

Things

/

List of things enrolled by this manufacturer. Filter by registrar. Enroll button. Provision button for selected things.

Thing Detail

/things/:thingId

Thing info, provision button (if ENROLLED), join code + QR display (if PROVISIONED), serial number management.

Settings

/settings

Registrar management. List registrars owned by this account. Create new registrar (triggers approval request). Status badges (PENDING/ACTIVE/REJECTED).

Profile

/profile

User profile.

Enrollment flows:

  1. With serial numbers — manufacturer integrates via API, provides SNs from their system → things can be provisioned immediately

  2. Without serial numbers — enroll by model/type + quantity → things created as ENROLLED (draft), provisioned later manually

Arsenal

Purpose: Platform admin console for registrar governance and access-request management.

Location:  stargate.flexgalaxy.ai/arsenal
Backend:   ThingHub API (cross-cluster via admin-gateway proxy)
KC Client: arsenal (master realm)
Port:      5191 (local dev)

Pages:

Page

Route

Description

Registrars

/

List all registrars. Filter by status. Approve/reject pending requests. View registrar details.

Access Requests

/access-requests

List registrar access requests. Approve/refuse pending requests.

Profile

/profile

User profile.

API design

New endpoints (ThingHub backend)

Registrar API (/api/v1/registrars):

Method

Path

Description

POST

/api/v1/registrars

Create registrar (manufacturer). Sets status=PENDING.

GET

/api/v1/registrars

List registrars. Filter by account_id, status.

GET

/api/v1/registrars/{id}

Get registrar detail.

PUT

/api/v1/registrars/{id}/approve

Approve registrar (platform admin only). Sets status=ACTIVE.

PUT

/api/v1/registrars/{id}/reject

Reject registrar (platform admin only). Sets status=REJECTED.

Registrar Access Request API (/api/v1/registrar-access-requests):

Method

Path

Description

POST

/api/v1/registrar-access-requests

Request access to a registrar (tenant).

GET

/api/v1/registrar-access-requests

List requests. Filter by registrar_id, account_id, status.

PUT

/api/v1/registrar-access-requests/{id}/approve

Approve access (platform admin only).

PUT

/api/v1/registrar-access-requests/{id}/refuse

Refuse access (platform admin only).

Repository API (/api/v1/repository):

Method

Path

Description

GET

/api/v1/repository/things

List things in the caller’s repository (with optional attr.* filters).

POST

/api/v1/repository/things

Bulk add things to repository by thing_ids (requires registrar access per thing).

Saved Filter API (/api/v1/saved-filters):

Method

Path

Description

GET

/api/v1/saved-filters

List saved filters for the current account.

POST

/api/v1/saved-filters

Create a saved filter (name, registrar_ids, filter conditions).

GET

/api/v1/saved-filters/{id}/things

Execute saved filter — live query across referenced registrars.

DELETE

/api/v1/saved-filters/{id}

Delete a saved filter.

Scope Things API (/api/v1/scopes/{id}/things):

Method

Path

Description

GET

/api/v1/scopes/{id}/things

List things in a scope.

POST

/api/v1/scopes/{id}/things

Add a repository thing to a scope.

DELETE

/api/v1/scopes/{id}/things/{scopeThingId}

Remove a thing from a scope.

Provisioning endpoint (/api/v1/things):

Method

Path

Description

POST

/api/v1/things/{thingId}/provision

Provision a thing. Generates join_code (NanoID). Sets status=PROVISIONED.

POST

/api/v1/things/batch-provision

Batch provision multiple things.

Modified join endpoint:

POST /api/v1/things/join now accepts either access_code (existing) or join_code (new NanoID from QR).

Public API (via KrakenD)

ThingMake’s public API for manufacturer system integration is exposed via KrakenD at api.flexgalaxy.ai:

POST   /things/v1/things                    → Enroll thing(s)
POST   /things/v1/things/{id}/provision      → Provision thing
POST   /things/v1/things/batch-provision     → Batch provision
GET    /things/v1/things                     → List things (with filters)
GET    /things/v1/things/{id}                → Get thing detail

Access control summary

Action

Who

How

Create registrar

Manufacturer (with enroll_things capability)

ThingMake Settings → Arsenal approves

Approve/reject registrar

Platform admin

Arsenal

Enroll things

Manufacturer (with enroll_things capability)

ThingMake → select registrar → enroll

Provision things

Manufacturer / device / API

ThingMake or API → generates join_code

Add thing to repository (access_code)

Any authenticated user

Scan QR code or enter access_code → POST /api/v1/things/join

Add things to repository (bulk)

Account with registrar access

Browse registrar → select things → POST /api/v1/repository/things

Request registrar access

Any tenant account

Request access → Arsenal approves

Approve/refuse access request

Platform admin

Arsenal

Create OTA target filter

Account with registrar access + manage_ota_rollouts

Filter over accessible registrar things

All authorization checks are delegated to the DotID policy-engine — see ADR-0004 and roles and access.

Migration strategy

This is a hard cutover — enrollment functionality is removed from ThingHub and moved to ThingMake.

Database migrations:

  1. V6: Create registrars table

  2. V7: Create registrar_access_requests table

  3. V8: (removed — batch join tokens deprecated in favor of bulk repository add via registrar access)

  4. V9: Add join_code and provisioned_at to things table

  5. V16: Create registrar_saved_filters table

  6. Data migration: For existing things with registrar_id set to an account ID, create a default Registrar entity per unique account and update the FK

The existing registrar_id column on things transitions from storing an account ID string to storing a Registrar entity UUID.