Ark 2.0.0 → 3.0.0 — Release Run (BRE → Connector Routing)
⓿ INFRA ─▶ ① DB: migrate ─▶ ② START apps ─▶ ③ ADMIN API + config ─▶ ④ DB: backfill ─▶ ⑤ TEST
This release replaces the platform BRE with Connector Routing. twilio and
message-media are available as part of the v3.0.0 release, but are not
enabled by default — clients should contact their Digizoo sales representative to
arrange access.
Ark Flows (ark-flows-api / ark-flows-worker), the Business Rules Engine
(ark-connector-bre), and the Temporal setup they depend on are optional in
this release. The steps below that cover them — the Temporal infra, the
ark-flows / ark-connector-bre migrations, and the BRE rule / worker
registration — can be skipped for a base deployment. Clients who want to use these
components will need to update their own setup and configuration to support them;
contact your Digizoo representative to plan this.
Back up every Ark database before you start. This release includes destructive, non-reversible migrations. If anything goes wrong during the migration, roll back by re-deploying the backed-up database — there is no in-place undo.
Legend — two kinds of steps
| Tag | Meaning |
|---|---|
| SQL | Run against the named database. Universal — run for every environment. |
| Bruno | Admin API request (collection release-3.0.0). Universal — run for every environment. |
| Local deploy | A package.json script. This is how we migrate / start / test locally. In a real environment these actions are performed by that environment's own deployment pipeline (image build, rollout, migrate deploy on container startup). They are listed here only so the local run is reproducible — do not treat the pnpm commands as the deployment mechanism for dev/uat/prod. |
Local vs pipeline. The SQL and Bruno steps are run by hand in every environment (local:
sqlcmd/bcp/ Bruno; Azure: portal Query Editor or Cloud Shell + Bruno). The **** steps are local only — in dev/uat/prod the deployment pipeline performs them (image build → rollout →migrate deployon container startup → test stage). Do not treat thepnpm/dockercommands as the dev/uat/prod deployment mechanism.
Local-run working directory (clean worktree with the Bruno collection + :release scripts):
cd <your-local-checkout>
Start infra — ensure Temporal is up (optional — Ark Flows only)
The flows-worker connects to Temporal on localhost:7233. Temporal is new in 3.0.0 — your 2.0.0 infra (db + simulator + tracing) doesn't have it, so it's the missing piece.
Local deploy — start only the Temporal services (real envs: managed service, already running):
docker compose up -d temporal temporal-ui
Start Temporal explicitly — do not run bare
docker compose up -d/pnpm start:docker-liteon the whole stack, and neverdocker compose down: thedbservice has no volume, so recreating/removing it deletes your 2.0.0 data.
Wait until Temporal is listening (auto-setup takes ~30–60s; the worker does not retry):
until nc -z localhost 7233; do echo "waiting for Temporal..."; sleep 2; done
Local deploy — (re)load simulator mocks. Local/dev only — locally the connectors' outbound vendor calls are intercepted by the simulator and answered from mock-rules.json; in Azure dev/uat/prod the connectors hit the real vendors, so this step is skipped there. The simulator is already running; re-upload this branch's mocks (overwrite_existing=true, safe to re-run):
SIMULATOR_URL=http://localhost:3001 ./scripts/upload-mock-rules.sh -f ./simulator/mock-rules.json
DB — Pre-flight + Migrations
Never
db:init/migrate reset— it wipes your data.
Local deploy — prepare the workspace (real envs: handled by the build pipeline):
pnpm install
pnpm setup-envs:local
SQL — database ark-platform — confirm notifications empty, and record clientId / brandId for Step :
SELECT COUNT(*) FROM dbo.Notification;
SELECT clientId, brandId, slug FROM dbo.Client;
SQL — server level — idempotent; ensures the three release databases exist (skips any already present). Connect to any DB (e.g. master):
IF DB_ID('ark-connector-simplekyc') IS NULL CREATE DATABASE [ark-connector-simplekyc];
IF DB_ID('ark-connector-bre') IS NULL CREATE DATABASE [ark-connector-bre];
IF DB_ID('ark-flows') IS NULL CREATE DATABASE [ark-flows];
Local deploy — apply migrations (real envs: each app runs prisma migrate deploy in its container entrypoint on rollout):
pnpm --filter @digizoo/ark-platform exec prisma migrate deploy
pnpm --filter @digizoo/ark-connector-bre exec prisma migrate deploy
pnpm --filter @digizoo/ark-connector-simplekyc exec prisma migrate deploy
pnpm --filter @digizoo/ark-flows-api exec prisma migrate deploy
SQL — database ark-platform verify the five CR tables exist and BreConfig / Rule are gone:
SELECT name FROM sys.tables
WHERE name IN ('Connector','ArkEntity','Binding','ConnectorPolicy','ConnectorPolicyCondition');
Start Apps
Local deploy — start apps without the two notification connectors (real envs: the deployment rolls out the release images, excluding twilio / message-media):
pnpm start:tsgo:release
Bruno — release-3.0.0 → env Local → run 00 Health readiness.
Admin API — Catalog + Policies
Do not run
pnpm configure:local— it registers all 7 connectors.
Bruno — collection release-3.0.0, environment Local, folder Local — run in order:
01 Register ultracs_v1
02 Register greenid_v1
03 Register simplekyc_v1
04 Register kleber_v1
05 Register bre
06 Verify connectors ✅ expect exactly those 5 (no twilio / messagemedia)
07 Upload tenant policies
08 Verify policies ✅ expect the policy capability keys
Token is fetched automatically (OAuth2 client-credentials from the
AUTH_MICROSOFT_ENTRA_*env vars). For dev/uat/prod: select the matching environment folder; fill theTODO-set-*URLs/slug first.
·b — Upload the BRE rule (optional — Business Rules Engine only)
Do NOT run
configure-instance.sh/pnpm configure:local:releaseagainst dev/uat/prod. This is an upgrade of a live 2.0.0 instance: external-application config, client config, products and product-mappings already exist in every environment and are not redeployed. Re-running configure would re-touch live tenant config just to deliver one new rule — needless blast radius (and no pipeline does this:SKIP_CONNECTOR_ROUTINGis set only by the localconfigure:local:releasescript).
Step 11 uploads the connector-BRE rule risk-scoring-individual, which ark-flows
onboarding evaluates at runtime; all three flows (individual / company / sole-trader) resolve
to this single rule in v1 (COMPANY_RISK_RULE_KEY / SOLE_TRADER_RISK_RULE_KEY both map to
it). Without it, ark-flows onboarding fails — prod included.
Bruno — release-3.0.0 → matching env → run 11 Upload BRE rule (risk-scoring-individual).
Idempotent upsert (PUT …/bre-rules/risk-scoring-individual, body {name, description, jdm, status:"ACTIVE"} — ruleKey in the path). Expect 200/201.
Verify a policy routes — Bruno → matching env → 10 Verify policy (address autocomplete) (no party needed; G2). Expect 200; CRT022003 ⇒ policy missing for address.suggest.
·c — Register the Temporal worker external application (optional — Ark Flows only)
The ark-flows-worker (Temporal onboarding worker) calls the platform with an Entra
client-credentials token. The token's audience is validated by the platform's Entra
strategy (fixed at the config layer — AUTH_MICROSOFT_ENTRA_SCOPES points at the platform
API id), but authorization is a separate DB lookup: ExternalApplicationAccessGuard
reads the token's appid claim and requires both an ExternalApplication row for that
oAuthAppId and an ExternalApplicationClientBrandMapping to the called tenant.
Without both rows the worker's calls fail 403
AUT001002at runtime — even with a valid, correctly-scoped token. This is a separate provisioning step from the configure path (which only re-touches existing external-app config — see ·b); a fresh worker registration is not delivered by configure and must be run here.
Bruno — release-3.0.0 → matching env → run 12 Register ark-flows-worker external app then 13 Map worker to tenant.
12 Register ark-flows-worker external app PUT …/external-applications/{workerOAuthAppId} body { "slug": "ark-flows-worker" }
13 Map worker to tenant (client/brand) PUT …/external-applications/{workerOAuthAppId}/clients/{clientSlug} (no body)
Fill the env var
workerOAuthAppIdfirst — it is the worker'sAUTH_MICROSOFT_ENTRA_TOKEN_CLIENT_ID(the token'sappid), per environment, and must be a UUID. Dev value (infra/k8s/overlays/dev/ark-flows-worker/configmap.yaml):<redacted-guid>. uat/prod use their own worker app-registration ids.
12is an idempotent upsert keyed onoAuthAppId; body is{ slug }only (the controller ignores anything else). Expect 200. It creates theExternalApplicationrow but does not grant tenant access.13creates the client/brand mapping; no body,clientSlugmust resolve to an active tenant (SlugTenantGuard). Expect 200. Run once per tenant the onboarding flows serve.
DB — Binding Backfill (cross-database migration)
The (arkId → vendorId) pairs live in ark-connector-uda; the Binding rows must land in ark-platform. These are separate databases — and in dev/uat/prod, separate Azure SQL servers where cross-database queries are not supported. So the migration is always extract from UDA → load a platform staging table → transactional apply, never an in-place copy.
Requires the
ultracs_v1catalog row from Step (FK). Without the backfill, reads on pre-existing entities returnCRT004005.
Where
clientId/brandIdcome from (UDA doesn't store tenant):
- Accounts / Cards aren't stored on the platform at all (v2.0.0 has no Account/Card model) — their only tenant source is the single
ark-platform.dbo.Clientrow. Confirm there is exactly one tenant first (SELECT clientId, brandId, slug FROM dbo.Client;). More than one row ⇒ stop: accounts/cards can't be attributed from 2.0.0 data alone (needs business input).- Parties / Customers carry tenant on the platform (
dbo.Party/dbo.Customer, keyed byid = arkId) — the multi-tenant-safe source. (This is how the supersededEntityConnectorMappingmigration did it —MERGE … USING dbo.Party p … clientId = p.clientId.)
Source-controlled artifacts (run verbatim; the export queries double as the bcp query text):
- extract:
releases/3.0.0/cr-binding-migration/export-{customers,accounts,cards}.sql— run against UDA- create staging:
releases/3.0.0/cr-binding-migration/01-create-staging.sql· apply:releases/3.0.0/cr-binding-migration/02-apply-bindings.sql(transactional, idempotent, intoArkEntity+Binding)
Best practice (per SQL Server / Azure SQL guidance): stage first → validate row counts → set-based transactional apply, keep extract queries in source control. Both paths below honour that; they differ only in how the data crosses the DB boundary.
Use the column the platform actually binds as
vendorId(verified against the UDA connector mappers +routing.bindcalls — the backfill must reproduce exactly whataccount.open/party.create/card.createbound at runtime):
entity arkId column vendorId column Party PARTY_IDCLIENT_KEY(party.service.ts:86→vendorId: createdClient.clientKey)Account ACCOUNT_ARK_IDCORE_BANKING_NUMBER(accounts.mapper.ts:15→vendorId: CORE_BANKING_NUMBER) — notACCOUNT_NUMBERCard CARD_ARK_IDCARD_ID(cards.mapper.ts:72→vendorId: udaCard.Key)
Path A — same SQL instance · LOCAL docker only
Locally both DBs share one instance, so a 3-part-name cross-database read is valid. SQL — database ark-platform:
USE [ark-platform];
IF OBJECT_ID('dbo.CrBindingStaging','U') IS NULL
CREATE TABLE dbo.CrBindingStaging (
arkId UNIQUEIDENTIFIER NOT NULL, connectorId VARCHAR(64) NOT NULL,
vendorId VARCHAR(256) NOT NULL, clientId NVARCHAR(254) NOT NULL, brandId NVARCHAR(254) NOT NULL);
DECLARE @clientId NVARCHAR(254) = '<clientId>';
DECLARE @brandId NVARCHAR(254) = '<brandId>';
INSERT INTO dbo.CrBindingStaging SELECT '{'+ACCOUNT_ARK_ID+'}','ultracs_v1',CORE_BANKING_NUMBER,@clientId,@brandId FROM [ark-connector-uda].dbo.Account WHERE DELETED=0;
INSERT INTO dbo.CrBindingStaging SELECT '{'+CARD_ARK_ID+'}','ultracs_v1',CARD_ID,@clientId,@brandId FROM [ark-connector-uda].dbo.Card WHERE DELETED=0;
INSERT INTO dbo.CrBindingStaging SELECT '{'+PARTY_ID+'}','ultracs_v1',CLIENT_KEY,@clientId,@brandId FROM [ark-connector-uda].dbo.Party WHERE DELETED=0;
Then run 02-apply-bindings.sql (set @migrator) — see "Apply" below — and verify (below).
Path B — separate servers · dev / uat / prod (professional process)
No cross-server query — move the data with bcp (extract → transport → load → apply). Run from a host with network access to each server.
B1 · Extract from UDA — one file per entity, clientId/brandId baked in (these SELECTs are the export-*.sql bodies):
UDA="tcp:<uda-server>.database.windows.net"; UDB="ark-connector-uda"; U="<user>"; P="<pwd>"
CID="<clientId>"; BID="<brandId>" # add -G and -U <user>@<tenant> for Entra auth
bcp "SELECT '{'+ACCOUNT_ARK_ID+'}','ultracs_v1',CORE_BANKING_NUMBER,'$CID','$BID' FROM dbo.Account WHERE DELETED=0" queryout accounts.dat -c -t"|" -S "$UDA" -d "$UDB" -U "$U" -P "$P"
bcp "SELECT '{'+CARD_ARK_ID+'}','ultracs_v1',CARD_ID,'$CID','$BID' FROM dbo.Card WHERE DELETED=0" queryout cards.dat -c -t"|" -S "$UDA" -d "$UDB" -U "$U" -P "$P"
bcp "SELECT '{'+PARTY_ID+'}','ultracs_v1',CLIENT_KEY,'$CID','$BID' FROM dbo.Party WHERE DELETED=0" queryout parties.dat -c -t"|" -S "$UDA" -d "$UDB" -U "$U" -P "$P"
B2 · Transport the .dat files over a secure channel (they contain identifier mappings — treat as confidential; delete after load).
B3 · Stage in ark-platform — first run 01-create-staging.sql to create dbo.CrBindingStaging, then bulk-load:
PLAT="tcp:<platform-server>.database.windows.net"; PDB="ark-platform"
bcp dbo.CrBindingStaging in accounts.dat -c -t"|" -S "$PLAT" -d "$PDB" -U "$U" -P "$P"
bcp dbo.CrBindingStaging in cards.dat -c -t"|" -S "$PLAT" -d "$PDB" -U "$U" -P "$P"
bcp dbo.CrBindingStaging in parties.dat -c -t"|" -S "$PLAT" -d "$PDB" -U "$U" -P "$P"
B4 · Validate (ark-platform) — staged rows must equal the export totals:
SELECT COUNT(*) FROM dbo.CrBindingStaging;
For hardening / repeatability, generate a
bcpformat file (bcp dbo.CrBindingStaging format nul -c -t"|" -f staging.fmt ...) and pass-f staging.fmton both ends. Scale note:bcpis right for a one-off cutover of modest volume; for very large or recurring moves use an Azure Data Factory copy activity (UDA → staging) — same stage-then-apply shape.
Apply (both paths) — section (b)
SQL — database ark-platform — set @migrator at the top of 02-apply-bindings.sql, then run the whole file (transactional; idempotent on (arkId, connectorId, vendorId); rolls back atomically on any guard failure):
DECLARE @migrator NVARCHAR(254) = 'cr-migration-3.0.0';
Verify + cleanup (both paths)
SQL — database ark-platform (direct DB check — no headers / party token):
SELECT COUNT(*) AS bindings FROM dbo.Binding WHERE connectorId = 'ultracs_v1';
SELECT ae.clientId, ae.brandId, COUNT(*) AS bindings
FROM dbo.Binding b JOIN dbo.ArkEntity ae ON ae.arkId = b.arkId
GROUP BY ae.clientId, ae.brandId;
-- once verified:
TRUNCATE TABLE dbo.CrBindingStaging;
Then securely delete the .dat files (Path B).
The count should equal staged rows (accounts + cards + parties).
Bruno (optional API spot-check) — release-3.0.0 → matching env → 09 Verify binding (party read). First set the env var partyId to a migrated party arkId:
SELECT TOP 1 p.id FROM dbo.Party p JOIN dbo.Binding b ON b.arkId = p.id WHERE b.connectorId='ultracs_v1';Expect 200;
CRT004005⇒ binding missing. Needs a CLIENT-role token +clientid/brandid/partyidheaders (set in the env) — if it401s, the token lacksCLIENT; rely on the Step suite instead.
Run ark-quality
Local deploy — run the e2e suite excluding notification features (real envs: the test stage runs against the deployed environment):
pnpm test:e2e:release
Expect ~2 failures when running the full suite against a migrated DB (this flow, which skips
db:initto preserve client data). They depend onprisma/seed.tsfixtures that onlydb:initcreates — seed-fixture gaps, not release/migration defects:
connector-routing-admin/cross-tenant.feature— needs the seeded tenant-BClientApiKey(elseGEN001004 Invalid API key).parties/delete-party.feature"Party ID mismatch" — needs the seeded TRUST party…440004(else404not409).The migration itself is validated by the binding checks in Step . For a fully green board, run the suite on a fresh
db:init-seeded DB (code validation) separately from the migrated DB (data validation).