Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
20 KiB
20 KiB
Scratchpad — Appliance registration → download → install flow
- Plan slug:
appliance-registration-install-flow - Created:
2026-06-05 - Source spec:
docs/superpowers/specs/2026-06-05-appliance-registration-install-flow-design.md
What This Is
Working memory for the register → download → install (+ re-issue) flow. The spine
is a direction inversion: today the appliance generates its own tenant UUID and
sends it up at /register; the new flow mints tenant_id upstream at registration,
carries it down via a one-time install code, and the appliance adopts it via a
new INITIAL_TENANT_ID.
Decisions
- (2026-06-05) Reuse
/registerfor the friendly install code (decision 1) — one redeem path, reuse the claim-code machinery. - (2026-06-05) Extend
claim_codes(decision 2):entitlement_id→ nullable, addtenant_idFK →tenant_registry. One code type for essentials (no entitlement) and paid. Chosen over a separateinstall_tokenstable. - (2026-06-05) Presigned for all downloads (decision 3) — registration-gated, not public; ISO isn't a secret, the code is the gate.
- (2026-06-05) Re-issue in scope now (decision 4) — portal action resolves an
existing registry tenant and mints a fresh code for the same
tenant_id. - (2026-06-05) Test weighting is smoke-first (user directive): small automated
set for the riskiest seams, everything else validated live on the VM. This
deliberately inverts the software-planner default (tests longer than features) —
tests.jsonis shorter and smoke-weighted on purpose. - (2026-06-05) The short code is the identity carrier for BOTH tiers (not just
paid). Essentials installs get a short code too and never type a raw tenant_id;
the code's
tenant_idis set for everyone,entitlement_idis null for essentials. One-liner framing: "a short code always resolves a tenant; for paid it additionally mints a bound license." - (2026-06-05) §16 open questions all resolved: (1) alga-license presigns with
object-store creds via env vars; the presigned URL is emailed to the user
after registration/purchase (alongside the code) — not a public link. (2) Paid
ordering: registry row at form submit, entitlement + code on
checkout.session.completed. (3) Essentials edition fromtenant_registry.edition(no edition on the code). (4) ReusegenerateClaimCode()for install codes. Remaining impl detail (not a fork): which object-store key holds the current generic ISO + how the release process publishes it (F041).
Discoveries / Constraints
- (2026-06-05)
claim_codesbaseline isentitlement_id NOT NULL(alga-license/migrations/03_claim_codes.cjs). Needs a new migration to relax it and addtenant_id. - (2026-06-05) alga-license accessors that already exist (
src/db/db.ts):createRegistryTenant,getRegistryTenant,getRegistryTenantByEmail,insertClaimCode,getClaimCode,consumeClaimCode,revokeClaimCodesForEntitlement,upsertAppliance(already takestenantId),setEntitlementLicenseSub,getEntitlementById. Signing helpers exist:signLicense(takesaud),generateClaimCode,generateApplianceCredential,generateLicenseId. - (2026-06-05) New alga-license work: the migration,
insertClaimCode+getClaimCode/ClaimCodeRowfortenant_id/nullable entitlement, a newrevokeClaimCodesForTenant(essentials reissue has no entitlement to key on), asetRegistryTenantInstalled, the/registeressentials+response changes, the/register-tenantand/install-codes/reissueroutes, and a MinIO presign helper. - (2026-06-05)
/registertoday (src/routes/register.ts): requires an entitlement (getEntitlementById→ 500 if none), bindsaudfrom the request bodytenant_id, returns{ appliance_credential, first_jwt, check_in_url }. The new flow sourcestenant_idfrom the code row and returns it (+ edition) to the appliance. - (2026-06-05)
/check-in(src/routes/checkIn.ts) already preservesappliance.tenant_idasaudacross re-signs — no change needed. - (2026-06-05) The appliance tenant UUID is born in
ee/server/src/lib/testing/tenant-creation.tscreateTenant(~line 80): it inserts intotenantswithout atenantvalue, so the DB default (gen_random_uuid()) generates it and it's returned via.returning('tenant'). That insert is the exactINITIAL_TENANT_IDseam. - (2026-06-05)
connectAppliance(server/src/lib/actions/licenseManagementActions.ts, ~line 125) already POSTs{ claim_code, appliance_id }to/registerand seedslicense_statewithfirst_jwt/appliance_credential/check_in_url— but AFTER the tenant exists. The install-time consumer generalizes this to run before create-tenant and adopt the returnedtenant_id. - (2026-06-05) Licval WIP already in the working tree (uncommitted, NOT part of
this plan's deltas — treat as precondition F001):
create-tenant.ts+tenant-creation.tsthreadINITIAL_ADMIN_PASSWORD/args.passwordthrough tocreateAdminUser(usesinput.password ?? generateSecurePassword()), initializetenant_settings(onboarding pending), and honorDB_HOST/DB_PORT/DB_USER_ADMIN.INITIAL_TENANT_IDlands in the same two files. Do not revert this WIP.
Build-step 1 — alga-license schema + /register (DONE on branch feat/registration-install-flow)
- (2026-06-05) Two refinements discovered while implementing (both fed back into
the PRD/features):
claim_codes.tenant_idis NULLABLE, not NOT NULL. Migration altersentitlement_idNULLABLE viaknex.raw('ALTER TABLE … DROP NOT NULL')(knex.alter()would try to rebuild the FK) and addstenant_id uuidnullable FK + index (migrations/05_claim_codes_registry.cjs)./registerresolvestenant = row.tenant_id ?? body.tenant_id— the body path is the clean legacy fallback, which only works because tenant_id is nullable.- Essentials
/registercreates NO appliance row.appliances.entitlement_idisNOT NULL(FK), and essentials has nothing to refresh (no license → no check-in), so the essentials branch returns{ tenant_id, edition }and does not callupsertApplianceor issue a credential. (Original F012 said "still upsertAppliance" — corrected.)
- (2026-06-05)
createRegistryTenantalready accepted a pinnedtenantIdand returns the row —/register-tenant(build step 3) is mostly wiring. - (2026-06-05)
RegisterResponsenow returnstenant_id+edition+company_name/contact_email;appliance_credential/first_jwt/check_in_urlare optional (paid only). The existing alga-psaconnectAppliancedestructures the three paid fields and ignores the rest, so paid back-compat holds. - (2026-06-05) Validated:
tsc --noEmitclean; migration applies + rolls back + re-applies on a throwawaypostgres:16(5433); jest 28/28 incl. 3 new seam tests (claim_codes tenant_id round-trip,revokeClaimCodesForTenant,setRegistryTenantInstalled). The repo'ssigning.test.tsIS the gated-on-DB_HOST pg integration suite — extend that block, don't add a parallel harness. - (2026-06-05) Test weighting note:
/registerHTTP behavior (T003/T004) is smoke, not Fastify route tests — validated in the build-step-4 live loop, per the light-automated directive.
Build-step 2 — INITIAL_TENANT_ID seam (code in working tree, alga-psa; UNCOMMITTED w/ licval WIP)
- (2026-06-05) Implemented F050/F051/F052/F054/F055:
createTenant(ee/server/src/lib/testing/tenant-creation.ts) takes optionaltenantId→ setstenantInsert.tenantwhen present (else DBgen_random_uuid()default, unchanged); idempotency guard returns the existing tenant (+ its client) if the id already exists so a re-run doesn't error/duplicate. Threaded throughTenantCreationInput→createTenantComplete.create-tenant.tsreadsINITIAL_TENANT_ID(env or--tenantId) and passes it down. - (2026-06-05) Three
tenant-creation.tscopies — onlyee/server/src/lib/testing/tenant-creation.tsis the live path (whatserver/scripts/create-tenant.tsimports).packages/ee/src/lib/testing/…is the CE stub (throws). No parallel edit needed. - (2026-06-05) No caller breakage: the change is additive-optional;
tenant-test-factory.ts(the only othercreateTenantCompleteconsumer) is unaffected. OthercreateTenant(matches are unrelated functions (temporal activities, server test-utils). - (2026-06-05) Not committed — these two files also carry the user's licval WIP (password seam). Left uncommitted in the working tree to avoid committing in-flight work; appliance changes (steps 2+4) commit together when the user is ready. F053 (bootstrap passes the redeemed id) + full typecheck land in step 4.
- (2026-06-05) T007/T008 reclassified AUTO→SMOKE: exercising
createTenant's id adoption needs the full alga-psatenants/clientsschema (heavy), so it's validated in the live install loop, not a unit DB.
Build-step 3 — /register-tenant + /install-codes/reissue + presign (DONE, alga-license branch)
- (2026-06-05) Dependency-free SigV4 presigner (
src/storage/presign.ts) — the service had no AWS SDK and is intentionally lean (fastify/knex/pg only), so the S3/MinIO presigned-GET URL is hand-rolled withnode:crypto(path-style, UNSIGNED-PAYLOAD, query-auth).nowis injectable for deterministic tests. - (2026-06-05) Object-store config is env-driven and OPTIONAL:
OBJECT_STORE_ENDPOINT/REGION/BUCKET/ACCESS_KEY/SECRET_KEY+APPLIANCE_ISO_KEY(defaultappliance/current/alga-appliance.iso).getPresignConfigFromEnv()returns null when unset so existing deploys (and /sign /register /check-in) boot without it; only register-tenant/reissue need it, and they emitdownload_url:''if it's missing. TTL =claimCodeTtlSeconds(link expiry aligned to the code). - (2026-06-05) New routes:
/register-tenant(service-authed) creates the registry row, (paid) upserts the entitlement bound to the new tenant, mints the install code carrying tenant_id (+ entitlement), presigns./install-codes/reissueresolves by tenant_id|email,revokeClaimCodesForTenant, re-attaches the active entitlement via newgetActiveEntitlementByTenant, mints a fresh code + link. - (2026-06-05) Validated: tsc clean; jest 31/31 (added presign structural test ×2 + getActiveEntitlementByTenant). Route HTTP behavior (T010/T018) is smoke.
- (2026-06-05) DEPLOY FOLLOW-UP (relates to F041): the alga-license Deployment
(service
k8s/deployment.yaml+ nm-kube-config) must get theOBJECT_STORE_*env +APPLIANCE_ISO_KEY, and the appliance release process must publish the current ISO to that key. Not wired here (ops); register-tenant returns an empty download_url until it is.
Build-step 4 — appliance setup integration (MAPPED; needs coordination, NOT yet built)
- (2026-06-05) The redeem belongs in the host-service
setup-engine.mjs(has the form inputs + network egress; mints the in-cluster secrets), not the in-appconnectAppliance(which runs post-tenant). Seam: afternormalizeInitialTenant, if an install code is present, POST it toALGA_LICENSE_SERVICE_URL/register→ capturetenant_id+edition(+ paidfirst_jwt/appliance_credential/check_in_url), then:- add
INITIAL_TENANT_IDto theappliance-initial-tenantSecret (initialTenantSecretYaml, ~line 429) → create-tenant adopts it (build-step 2); - feed the
appliance-license-seedSecret (licenseSeedCmd, ~line 1014:EDITION_CHOICE+ optionalLICENSE_TOKEN).
- add
- (2026-06-05) THREE reconciliation decisions block a clean edit (all inside the
user's ACTIVE licval license-setup rework):
- UX: the setup UI (
status-ui/app/setup/page.tsx) already haseditionChoice(ee/ce) + a pastedlicenseKey. Does the install code become the primary path (overriding those), an added third option, or replace them? - Taxonomy: registry
edition = essentials|pro|premiumvs applianceeditionChoice = ee|ce+ license tierpro|premium. Need a mapping (essentials→ce? pro/premium→ee+token?). - Connected seed:
appliance_credential/check_in_urlare written today only by in-appconnectAppliance, not the seed Secret — install-time connected licensing needs new seed plumbing inpackages/licensing/src/lib/license-state.ts.
- UX: the setup UI (
- (2026-06-05)
setup-engine.mjs+status-ui/are NOT in the uncommitted WIP (safe to edit git-wise) but are conceptually the licval subsystem. F053/F060–F068 pend on the three decisions above.
Build-step 4 — appliance setup integration (BUILT in working tree, alga-psa)
- (2026-06-05) Redeem lives in the host-service. New
install-code.mjs(redeemInstallCode/deriveApplianceId/licenseSeedFromRedeem) is pure + unit-tested (7 tests, mock fetch).setup-engine.mjsvalidateSetupInputstakesinstallCode;applyRuntimeValuesAndReleaseSelectionredeems it (injectable viaoptions.redeemInstallCodefor tests) BEFORE building the two Secrets, threadingtenant_id→initialTenantSecretYaml(initialTenant, tenantId)(INITIAL_TENANT_ID line) and the license fields → theappliance-license-seedliterals (licenseSeedFromRedeem). Redeem failure →preflightFailureblocks the install (F067/F068). 2 new workflow tests; 8/8 pass. - (2026-06-05)
appliance-bootstrap-configmap.yaml: create-tenant getsINITIAL_TENANT_ID="${INITIAL_TENANT_ID:-}"(create-tenant.ts reads it; empty => DB-generated). License-seed SQL extended: asql_value()helper + new columnsappliance_id/check_in_url/appliance_credential(connected refresh), and the trial is suppressed whenINSTALL_EDITION=essentials(essentials resolves to the essentials tier, not an auto premium trial). - (2026-06-05) status-ui
setup/page.tsx: an Install code field (primary, recommended) above the manual Edition/license fallback; included in the POST. Typechecks clean. - (2026-06-05) DEFAULTS I PICKED — confirm during smoke (you authorized
sensible defaults + flag):
- Taxonomy: the appliance always runs EE (
EDITION_CHOICE=ee); essentials = EE-unlicensed (no token, no auto-trial), pro/premium = EE + minted token. Thecechoice stays only for a manual community install (no install code). - Connected seed:
appliance_id/check_in_url/appliance_credentialare seeded straight intolicense_stateby the bootstrap SQL (vs. the in-appconnectApplianceplaintext write) so daily check-in works from first boot.
- Taxonomy: the appliance always runs EE (
- (2026-06-05) Two caveats: (a) the install code is single-use and consumed
at apply time — a failed install past that point needs a re-issued code (the
reissue endpoint exists; consider persisting the redeem to skip re-redeem on
retry). (b)
ee/appliance/ubuntu-iso/overlay/.../host-service/is a build-synced COPY — the ISO build must pick up the newinstall-code.mjs+ setup-engine edits (overlay sync, not a manual edit).
Build-step 5 — nm-store (separate repo /home/robert/nm-store; outward-facing)
- (2026-05-31/06-06) nm-store is local. Its existing order flow is hosted-only
(checkout → Temporal
OrderInstallWorkflow→ hosted tenant; no appliance/ deploymentType concept). Appliance registration is a new outward-facing product surface there. - (2026-06-06) BUILT (branch
feat/appliance-registration-client, commit 6ab0a427):src/lib/algaLicenseClient.ts— typedregisterTenant()+reissueInstallCode()calling/register-tenant+/install-codes/reissue(BearerALGA_LICENSE_SERVICE_SECRET,ALGA_LICENSE_SERVICE_URL). This is the transport F071/F075 build on. Logic validated via a tsx harness (the repo's vitest can't run here — missingstrip-literalin node_modules under Node 24; the.test.tsis correct and runs once that dep gap is fixed). - (2026-06-06) BUILT — commerce surface (decisions: SEPARATE route + essentials+paid),
branch
feat/appliance-registration-clientcommitee53d0b0, tsc clean:/order/applianceregistration form (client): essentials → direct register → show code; paid (pro/premium) → embedded Stripe checkout./order/appliance/reissue.lib/appliance/applianceRegistration.ts:registerApplianceAndEmail,reissueApplianceAndEmail,sendInstallCodeEmail(Resend, reuses the newsletter pattern), and idempotentprovisionApplianceFromCheckoutSession— nm-store has no DB, so it writes the mintedtenant_id/install_codeback onto the Stripe subscription metadata and reuses them on thank-you refresh (so a refresh never re-mints).lib/appliance/applianceCheckout.ts: embedded subscription checkout stampingdeploymentType=appliance+ edition/contact into metadata, dedicatedreturn_url=/order/appliance/thank-you. Hosted OrderForm/checkout/thank-you untouched (the separate-route choice).- Checkout-complete is handled by the thank-you page (Stripe
return_url), not a webhook — so the appliance thank-you provisions on render. - Email is best-effort (Resend): logs + returns
emailSent:falseifRESEND_API_KEYunset; the code is always shown on-page too.
- (2026-06-06) nm-store env to set for PAID:
STRIPE_PRICE_ID_APPLIANCE_{PRO,PREMIUM}_{MONTHLY,YEARLY}(+ optional..._USER_MONTHLY/YEARLYper-user),ALGA_LICENSE_SERVICE_URL,ALGA_LICENSE_SERVICE_SECRET,RESEND_API_KEY/APPLIANCE_FROM_EMAIL. Essentials needs no Stripe price. Tests can't run here (repo viteststrip-literalgap +server-onlyblocks tsx) — validated by tsc + the tsx client harness + smoke. - (2026-06-06) F076 operator doc written:
ee/appliance/docs/registration-install-flow.md. - (2026-06-06) F041 remains ops: publish the current generic ISO to the
object-store key the presigner targets (
APPLIANCE_ISO_KEY), and wire theOBJECT_STORE_*env onto the alga-license Deployment (servicek8s/deployment.yaml- nm-kube-config).
Commands / Runbooks
- (2026-06-05) alga-license migration/db tests: run against a throwaway
docker run postgres:16(snap docker can't reach /tmp;docker runis fine — prior runs used port 5433). The least-priv app role can'tTRUNCATE(by design) — testsDELETE. DB-layer tests are gated onDB_HOST(skip without a DB). - (2026-06-05) Build gotcha: the shell has
NODE_ENV=production, sonpm installomits devDeps (breaks tsc/@types) — usenpm install --include=dev. - (2026-06-05) alga-license validation one-liner (throwaway pg):
docker run -d --name algalic-pg -e POSTGRES_PASSWORD=test -e POSTGRES_DB=alga_license -p 5433:5432 postgres:16thenDB_HOST=localhost DB_PORT=5433 DB_NAME=alga_license DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=test npm run migrateandDB_HOST=localhost DB_PORT=5433 DB_NAME=alga_license DB_USER_APP=postgres DB_PASSWORD_APP=test npm test. The signLicense tests need the alga-psa fixture key atpackages/licensing/src/lib/__test-fixtures__/v1-test.private.pem(present). - (2026-06-05) Appliance VM smoke: the full register→download→install loop is
validated live on the libvirt appliance VM (see the appliance teardown/reinstall
- VM ISO-test memories for driving setup via the browser / virsh).
Links / References
- Design spec:
docs/superpowers/specs/2026-06-05-appliance-registration-install-flow-design.md - Registry foundation (already shipped): alga-license PR #3/#4, nm-kube-config PR #50/#51.
- alga-license routes:
src/routes/{register,claimCodes,checkIn,sign,revoke}.ts; db:src/db/db.ts; migrations:migrations/0{1..4}_*.cjs; types:src/api-types.ts. - Appliance:
ee/server/src/lib/testing/tenant-creation.ts,server/scripts/create-tenant.ts,server/src/lib/actions/licenseManagementActions.ts.
Open Questions
- (2026-06-05) All four §16 design questions resolved — see the Decisions section. No open design forks remain. Only impl detail left: the object-store key for the current generic ISO + the release-process publish step (F041).