Architecture

System Context

Entitlements Service

system context entitlements service

Entitlements Mining

system context entitlements mining

The relinker worker and the POST /{user_id}/processor endpoint share one implementation in pkg/arraylink, and both hold an entitlement row across two network calls: resolving the user’s primary account at transactions-service, then posting the processor token to Array.

Those writes are whole-item puts, so they carry back exactly what was read and nothing else. A row that stopped being the active one during that window — retired in favour of a newer Array account, or removed by cleanup — must not be written back: the put would return it to Active and drop the ttl that schedules the old Array account for deletion, leaving that account with no cleanup path and the user with two active rows.

Both writes therefore go through dynamo.SaveIfActive, which requires the row to still exist and still be Active. A rejected write surfaces as arraylink.ErrEntitlementNotActive and is terminal on both paths: the worker records it and does not retry, and the endpoint answers 404. Array keeps the link either way — what has changed is which row owns it, and re-posting cannot make a retired row current again.

Signup is not affected. It builds a fresh row and owns a single unconditional write, so it has nothing stale to overwrite.

Array Account Cleanup

Deleting a live Array account is unrecoverable, so a cleanup record — never the entitlement row — carries the TTL that triggers one. Two things schedule one:

  • A cancellation, with a 63-day grace period (CLEANUP_TTL_DAYS), on a single key per user so that reactivating cancels it. Deleting that record is the only delete this service performs.

  • A second Array signup, scheduling the account the new row supersedes with a much shorter STALE_ENTITLEMENT_TTL_DAYS, default 1 day, on a key ending in the Array account id. Sitting off the cancellation key is what stops anything cancelling it: that account exists because the system made a second one, not because the user chose anything.

DynamoDB removing the expired record is the trigger, and the only row it ever takes. expiration-handler then deletes the Array.io account the record names — not whatever account the user holds now — and marks the entitlement row that named it Removed, found through GSI1. Entitlement rows are never deleted.

That last step is best effort. The record is already gone by the time the handler runs, so if the GSI1 lookup fails the account has still been deleted at Array, the row keeps its old status, and nothing remains to trigger another attempt.

No in-use check is needed: an account is only scheduled because a different one replaced it, and the same account arriving again returns at request time, so a record can never name the account in use.

A 404 from Array means the account is already gone, which is the outcome cleanup wanted, so it is reported as ErrArrayUserAlreadyDeleted and treated as success. The record that fired the event is already gone, so returning an error instead redelivers off the stream until it ages out, every attempt hitting the same 404.