Skip to main content

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.

Optional components

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

TagMeaning
SQLRun against the named database. Universal — run for every environment.
BrunoAdmin API request (collection release-3.0.0). Universal — run for every environment.
Local deployA 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 deploy on container startup → test stage). Do not treat the pnpm/docker commands 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-lite on the whole stack, and never docker compose down: the db service 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

Brunorelease-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 the TODO-set-* URLs/slug first.

·b — Upload the BRE rule (optional — Business Rules Engine only)

Do NOT run configure-instance.sh / pnpm configure:local:release against 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_ROUTING is set only by the local configure:local:release script).

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.

Brunorelease-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 AUT001002 at 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.

Brunorelease-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 workerOAuthAppId first — it is the worker's AUTH_MICROSOFT_ENTRA_TOKEN_CLIENT_ID (the token's appid), 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.

  • 12 is an idempotent upsert keyed on oAuthAppId; body is { slug } only (the controller ignores anything else). Expect 200. It creates the ExternalApplication row but does not grant tenant access.
  • 13 creates the client/brand mapping; no body, clientSlug must 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_v1 catalog row from Step (FK). Without the backfill, reads on pre-existing entities return CRT004005.

Where clientId / brandId come 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.Client row. 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 by id = arkId) — the multi-tenant-safe source. (This is how the superseded EntityConnectorMapping migration 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, into ArkEntity+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.bind calls — the backfill must reproduce exactly what account.open/party.create/card.create bound at runtime):

entityarkId columnvendorId column
PartyPARTY_IDCLIENT_KEY (party.service.ts:86vendorId: createdClient.clientKey)
AccountACCOUNT_ARK_IDCORE_BANKING_NUMBER (accounts.mapper.ts:15vendorId: CORE_BANKING_NUMBER) — not ACCOUNT_NUMBER
CardCARD_ARK_IDCARD_ID (cards.mapper.ts:72vendorId: 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 bcp format file (bcp dbo.CrBindingStaging format nul -c -t"|" -f staging.fmt ...) and pass -f staging.fmt on both ends. Scale note: bcp is 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/partyid headers (set in the env) — if it 401s, the token lacks CLIENT; 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:init to preserve client data). They depend on prisma/seed.ts fixtures that only db:init creates — seed-fixture gaps, not release/migration defects:

  • connector-routing-admin/cross-tenant.feature — needs the seeded tenant-B ClientApiKey (else GEN001004 Invalid API key).
  • parties/delete-party.feature "Party ID mismatch" — needs the seeded TRUST party …440004 (else 404 not 409).

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).