LinkO'Star

Data Model

Core entities and their relationships. For column-level entity schemas, check the component-schemas section of the OpenAPI spec — it regenerates with every backend deploy.

Relationships at a glance

Tenant (= Supplier)
  ├── SupplierBundle ── ProvisioningStep × N
  │        │           ── ManagedApp × N
  │        │           ── BundleDevice × N
  │        └─ issues ─▶ HubClaimCode ── (claim) ──▶ HubInstance
  │                                                  │  (metadata: V9 supplier-opaque)
  │                                                  ├── HubInstalledApp × N
  │                                                  ├── HubProvisioningRun × N  (step progress)
  │                                                  ├── HubTelemetry × N
  │                                                  ├── HubLog × N
  │                                                  └── HubAuthToken (1:1; refresh swaps access only)
  │
  └── ConsentSetting × N      DataPipeline × N      Persona / Access × N

Supplier (V10~V13)
  ├── SupplierOauthConfig (1:1)              ← platform-admin registered
  │
  └── SupplierLink × N  ─ User                ← n:1 (user × supplier × sub)
       ├── SupplierLinkState (in-flight PKCE)
       ├── SupplierPermissionCache × N        ← user × resource(HUB|DEVICE) × permissions
       └── SupplierRegistrationEvent × N      ← LinkOStar → supplier outbox

DeviceInstance (V5 / V8 / V9)
  ├── hub_uuid (V9, optional)                ← only customers who maintain the mapping
  ├── first_seen_hub_uuid (V5, audit)
  └── DeviceActivationEvent × N (V5 / V8)     ← sync_status: → sdx-web credit_ledger

Entities

Tenant

The top-level isolation unit. One supplier = one tenant. Every other resource carries a tenant_uuid column, and API requests scope themselves via X-Tenant-UUID.

SupplierBundle (출시 패키지)

The set of things a supplier wants applied at hub claim time. Has these children:

  • ProvisioningStep: onboarding wizard steps. Per type config (TEXT_INPUT, EXTERNAL_LIST_PICK, CONFIRM, INFO, CUSTOM).
  • ManagedApp: supplier-side apps the hub should install + keep up to date.
  • BundleDevice: device definitions to register alongside the bundle (auto_provision supported).
  • agentChannel: pins the hub-agent channel (stable/beta/canary) for hubs claiming with this bundle.

State machine: DRAFTPUBLISHEDARCHIVED. Only DRAFT allows mutations.

HubClaimCode

Single-use (or limited-use) code issued by a supplier. Carries supplier_uuid and bundle_uuid, so the claim handshake automatically applies the bundle. Statuses:

  • PENDING — issued, not yet consumed
  • USED — at least one hub consumed it
  • EXPIRED — policy expiry (e.g. 24h after issuance)
  • REVOKED — manually invalidated

V9 addition: metadata (JSON, opaque). The claim code creator can attach their own identifiers (owner / space / spot ids, order references, ...) as free-form JSON. LinkOStar never inspects it; at claim time it is copied verbatim to hub_instance.metadata so suppliers with their own backend (self-hosted suppliers) can track their mapping without ad-hoc storage.

HubInstance

Represents one physical hub (typically a Pi). Key columns:

  • hub_uuid (PK), tenant_uuid, supplier_uuid, bundle_uuid
  • device_instance_id: identifier sent by hub-agent (commonly a MAC-derived hash) — hub's self device
  • hub_identifier: human-friendly alias chosen at claim time
  • hub_type: OPEN / CLOSED
  • mac_address
  • metadata (JSON, V9): copied from the claim code at claim time. supplier-opaque.

DeviceInstance

LinkOStar's entitlement-counting unit. A tenant-scoped instance of a catalog device. The hub-device link is optional: only customers who want to keep that mapping populate the column.

  • device_instance_id (PK), tenant_uuid, device_uuid, device_version_uuid
  • activation_status: PENDING / ACTIVE / SUSPENDED / RETIRED
  • mac_address: PRE_REGISTERED matching key
  • first_seen_hub_uuid (V5): activation-time audit. Not a permanent mapping.
  • hub_uuid (V9, optional): current mapping. SaaS customers may use this; self-hosted-style customers leave it NULL.

DeviceActivationEvent (V5 / V8)

Audit row written when a DeviceInstance transitions to ACTIVE. V8 wired it up to the sdx-web outbound debit and added sync columns.

  • id, tenant_uuid, device_instance_id, device_uuid, hub_uuid (audit), activation_credit
  • sync_status (V8): PENDING → SYNCED / FAILED / REJECTED_BALANCE / BACKFILL_SKIPPED
  • synced_at, last_attempt_at, attempt_count, sync_error

An afterCommit hook fires ActivationSyncDispatcher, which POSTs to sdx-web's /api/m2m/billing/activation. Failures retry with backoff. Operators monitor via GET /platform/activation-events?syncStatus=....

SupplierOauthConfig (V10 / V11)

One row per supplier. Registered by platform-admin (PUT /platform/supplier-oauth-configs). OAuth 2.1 client config plus the V11 permission / registration sync URLs.

  • OAuth client: authorize_url, token_url, client_id, client_secret_encrypted (AES-GCM), scopes, userinfo_url, revoke_url
  • UI: consent_text, consent_text_locale_map
  • V11 sync: permission_query_url (if set, the supplier becomes the SSOT for permissions), hub_registration_url, device_registration_url, registration_shared_secret (HMAC, AES-GCM encrypted)

SupplierLink (V10)

User ↔ supplier account binding. n:1 model — the same LinkOStar user can link to multiple accounts of the same supplier.

  • UNIQUE KEY (user_uuid, supplier_uuid, supplier_user_id) — the n:1 discriminator is the OAuth sub
  • access_token_encrypted, refresh_token_encrypted, access_token_expires_at
  • consent_snapshot (JSON): the consent text + scopes at the moment the user accepted
  • Soft revoke: revoked_at

SupplierLinkState (V10)

In-flight PKCE state. Written by SupplierLinkService.beginAuthorize, deleted by the callback. 5-minute TTL; a scheduled job sweeps expired rows.

SupplierRegistrationEvent (V12 outbox)

Outbox row for LinkOStar → supplier registration pushes. Created in the afterCommit of a hub or device insert; the dispatcher posts it with an HMAC signature. Failures back off.

  • event_type: HUB_REGISTERED / DEVICE_REGISTERED
  • status: PENDING / SENT / FAILED / DEAD
  • UNIQUE KEY (supplier_uuid, idempotency_key) — at most one row per resource
  • attempt_count, next_attempt_at, last_error

SupplierPermissionCache (V13)

Caches the supplier's response when it is the permission SSOT (i.e. when permission_query_url is registered).

  • UNIQUE KEY (user_uuid, supplier_uuid, resource_type, resource_uuid)
  • resource_type: HUB / DEVICE
  • permissions (JSON array): owner / operator / viewer
  • fetched_at — combined TTL expiry + explicit webhook invalidation

HubAuthToken

One row per hub. Holds access_token + refresh_token. Refresh swaps the access token only. Incoming hub bearers are byte-compared against this row.

HubProvisioningRun

Created when a bundle expands at claim time. Per-step progress:

  • PENDING — awaiting user action
  • SUBMITTED — user input captured into submittedValue
  • SKIPPED — user skipped
  • FAILED — validation failed

HubTelemetry / HubLog

Hub-agent emits telemetry every ~60s; payload is stored verbatim as JSON in hub_telemetry.payload_json. Logs follow the same shape (hub_log). Timeseries index (hub_uuid, received_at).

UUID encoding

All UUID columns are BINARY(16) in MySQL. APIs serialise them as the canonical hyphenated string (e02bf423-22fc-4862-a8f8-bbf23ae9eccd). Treat them as strings on the integration side.

Timestamps

Every timestamp is ISO-8601 UTC (2026-06-04T12:34:56.789Z). The underlying column type is timestamp(3).