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_ledgerEntities
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: DRAFT → PUBLISHED → ARCHIVED. 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 consumedUSED— at least one hub consumed itEXPIRED— 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_uuiddevice_instance_id: identifier sent by hub-agent (commonly a MAC-derived hash) — hub's self devicehub_identifier: human-friendly alias chosen at claim timehub_type:OPEN/CLOSEDmac_addressmetadata(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_uuidactivation_status: PENDING / ACTIVE / SUSPENDED / RETIREDmac_address: PRE_REGISTERED matching keyfirst_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_creditsync_status(V8): PENDING → SYNCED / FAILED / REJECTED_BALANCE / BACKFILL_SKIPPEDsynced_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 OAuthsub access_token_encrypted,refresh_token_encrypted,access_token_expires_atconsent_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_REGISTEREDstatus: 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 / DEVICEpermissions(JSON array): owner / operator / viewerfetched_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 actionSUBMITTED— user input captured intosubmittedValueSKIPPED— user skippedFAILED— 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).