Claim-fenced row lifecycles in PostgreSQL, such as outbox claims and account access. The index lists every category.
Fences this application's decision-sensitive writes in the caller's transaction.
lockApplicationDecisionWithSql(
sql: DatabaseOperations,
applicationId: PublicApplicationId
): Effect.Effect<void, SqlError>- Inputs:
sql: DatabaseOperationsapplicationId: PublicApplicationId
- Output:
Effect.Effect<void, SqlError> - Errors:
SqlError - Requirements: none
- Side effects: Waits for application row custody and increments its fence version until commit or rollback.
- Source: packages/database/src/admissions/evidence.ts:28
How it works
Updates the application-owned version through the same SQL owner used by decision, correction and window backstops. Unlike a row lock alone, this invalidates old snapshots.
Use
yield* lockApplicationDecisionWithSql(sql, applicationId);Avoid
Calling outside the transaction that writes, or acquiring it before applicant custody.
Reads decoded admission evidence from the application's own period and semester.
readApplicationAdmissionEvidenceWithSql(
sql: DatabaseOperations,
applicationId: PublicApplicationId,
departmentId: DepartmentId,
options: { readonly lock: boolean }
): Effect.Effect<ApplicationAdmissionEvidence, SqlError | Schema.SchemaError | AdmissionOutcomeFailure>- Inputs:
sql: DatabaseOperationsapplicationId: PublicApplicationIddepartmentId: DepartmentIdoptions: { readonly lock: boolean }
- Output:
Effect.Effect<ApplicationAdmissionEvidence, SqlError | Schema.SchemaError | AdmissionOutcomeFailure> - Errors:
SqlError | Schema.SchemaError | AdmissionOutcomeFailure - Requirements: none
- Side effects: Reads evidence and, on writes, acquires versioned application custody until transaction end.
- Source: packages/database/src/admissions/evidence.ts:51
How it works
The SQL view owns live versus frozen citation selection and the public outcome projection. Historical uncited decisions retain their outcome and never acquire a current assessment citation. The caller owns authority, applicant custody and the transaction; lock=true takes the common fence.
Use
const evidence = yield* readApplicationAdmissionEvidenceWithSql(sql, applicationId, departmentId, { lock: true });Avoid
Treating a department selector as authority or observing eligibility before a lock wait.
Runs one explicit native provisioning attempt through Better Auth's public endpoint context API.
withNativeAuthProvisioning<A>(
context: Awaited<AuthEngine["$context"]>,
task: () => Promise<A>
): Effect.Effect<A, Cause.UnknownError>- Inputs:
context: Awaited<AuthEngine["$context"]>task: () => Promise<A>
- Output:
Effect.Effect<A, Cause.UnknownError> - Errors:
Cause.UnknownError - Requirements: none
- Side effects: Executes the caller's supported Better Auth write and scopes its private authority.
- Source: packages/database/src/auth-engine.ts:373
How it works
Only this helper mints the private marker. Hooks require both its identity and live owner registration; copied contexts, lookalike keys and request contexts grant no authority. The marker ends on success, failure or interruption. Better Auth's Promise writes have no cancellation ABI: interruption revokes later hook admission but does not certify rollback of an operation already accepted by its adapter. This helper retries nothing.
Use
yield* withNativeAuthProvisioning(context, () => context.internalAdapter.linkAccount(account));Avoid
Passing request-sourced authority or treating cancellation as a transaction receipt.
Whether the native account of personId exists and is not disabled.
accountAccessEnabled(
sql: DatabaseOperations,
personId: PersonId,
lock: "None" | "ForShare"
): Effect.Effect<boolean, SqlError>- Inputs:
sql: DatabaseOperationspersonId: PersonIdlock: "None" | "ForShare"
- Output:
Effect.Effect<boolean, SqlError> - Errors:
SqlError - Requirements: none
- Side effects: Reads
auth."user"; withForShareit holds a share lock on the row until the transaction ends. - Source: packages/database/src/identity-access.ts:38
How it works
It reads NOT access_disabled from the auth."user" row of personId, and a missing row
answers false. ForShare reads with a share row lock, so a disable, which
changeNativeAccountAccess writes under FOR UPDATE, waits until the transaction that
authorized with this answer ends. None reads without a lock, for a read that writes nothing.
Use
if (!(yield* accountAccessEnabled(sql, personId, "ForShare"))) return yield* fail("Denied");Avoid
Checking a command's actor with None, or in another transaction than its writes: a
disable can then commit between the check and the command. A command reads with ForShare
inside the transaction that writes.
SET list for the aggregate's claim UPDATE; targetAlias names the updated outbox row.
outboxClaimAssignments(
sql: DatabaseOperations,
targetAlias: string,
claimId: string,
claimedAt: string
): Statement.Fragment- Inputs:
sql: DatabaseOperationstargetAlias: stringclaimId: stringclaimedAt: string
- Output:
Statement.Fragment - Errors: none
- Requirements: none
- Side effects: none: it builds a fragment, and the aggregate's UPDATE writes the row.
- Source: packages/database/src/outbox-lifecycle.ts:144
How it works
The fragment sets status = 'Processing', the claim's claim_id and claimed_at, increments
attempts from the row that targetAlias names, and clears last_failure_tag. The aggregate
keeps the rest of its claim UPDATE: candidate selection, predecessor ordering, row locks, and
the RETURNING list. Attempts count claims; no other transition changes them.
Use
sql`UPDATE economy_receipt_outbox AS claimed SET ${outboxClaimAssignments(sql, "claimed", claimId, claimedAt)} FROM candidate WHERE claimed.effect_id = candidate.effect_id`;Avoid
Writing the claim columns by hand: a copy can miss the attempt count or keep the last failure tag, and it copies the column protocol that every outbox table shares. Put this SET list in every claim UPDATE.
Settles the claimed row as Delivered, with delivery evidence when the table records it.
markOutboxDelivered(
sql: DatabaseOperations,
table: OutboxTable,
claim: OutboxClaim,
evidence?: OutboxDeliveryEvidence
): Effect.Effect<void, OutboxClaimLost | SqlError>- Inputs:
sql: DatabaseOperationstable: OutboxTableclaim: OutboxClaimevidence?: OutboxDeliveryEvidence
- Output:
Effect.Effect<void, OutboxClaimLost | SqlError> - Errors:
OutboxClaimLost | SqlError - Requirements: none
- Side effects: Writes the outbox row that the claim still owns.
- Source: packages/database/src/outbox-lifecycle.ts:176
How it works
One UPDATE matches the row's effect_id, status = 'Processing', and the claim's claim_id.
It sets Delivered, clears the claim and last_failure_tag, writes delivered_at and
provider_reference when evidence carries them, and replaces payload_json with {} when
table.terminalPayload is Scrub. When no row matches, stale recovery or another claim owns
the row, so it changes nothing and fails with OutboxClaimLost.
Use
Database.use((sql) => markOutboxDelivered(sql, receiptOutbox, claim));Avoid
Updating the status by effect_id alone, or reporting a delivery after
OutboxClaimLost: the row belongs to another claim, whose outcome would be overwritten. Handle
OutboxClaimLost as a lost race, not as a success.
Settles the claimed row as Failed with its failure tag, so a later claim retries it.
markOutboxFailed(
sql: DatabaseOperations,
table: OutboxTable,
claim: OutboxClaim,
failureTag: string
): Effect.Effect<void, OutboxClaimLost | SqlError>- Inputs:
sql: DatabaseOperationstable: OutboxTableclaim: OutboxClaimfailureTag: string
- Output:
Effect.Effect<void, OutboxClaimLost | SqlError> - Errors:
OutboxClaimLost | SqlError - Requirements: none
- Side effects: Writes the outbox row that the claim still owns.
- Source: packages/database/src/outbox-lifecycle.ts:220
How it works
Under the same claim fence as markOutboxDelivered, it sets Failed and last_failure_tag,
clears the claim, and keeps payload_json, because a later claim delivers the same envelope.
When the claim no longer owns the row, it changes nothing and fails with OutboxClaimLost.
Use
Database.use((sql) => markOutboxFailed(sql, receiptOutbox, claim, failureTag));Avoid
Failing a row that no retry can deliver, such as an envelope that does not decode: a
later claim takes it again. Quarantine it with quarantineOutboxClaim.
Settles the claimed row as Quarantined, a terminal status, with its failure tag.
quarantineOutboxClaim(
sql: DatabaseOperations,
table: OutboxTable,
claim: OutboxClaim,
failureTag: string
): Effect.Effect<void, OutboxClaimLost | SqlError>- Inputs:
sql: DatabaseOperationstable: OutboxTableclaim: OutboxClaimfailureTag: string
- Output:
Effect.Effect<void, OutboxClaimLost | SqlError> - Errors:
OutboxClaimLost | SqlError - Requirements: none
- Side effects: Writes the outbox row that the claim still owns.
- Source: packages/database/src/outbox-lifecycle.ts:249
How it works
Under the same claim fence as markOutboxDelivered, it sets Quarantined and
last_failure_tag, clears the claim, and replaces payload_json with {} when
table.terminalPayload is Scrub. No claim selects a quarantined row again. When the claim no
longer owns the row, it changes nothing and fails with OutboxClaimLost.
Use
quarantineOutboxClaim(sql, notificationOutbox, { effectId, claimId }, failureTag);Avoid
Quarantining a failure that a retry can overcome, such as a provider outage: the effect
is then never delivered. Mark it failed with markOutboxFailed.
Returns an interrupted claim to Pending without a provider outcome; a lost claim needs none.
releaseOutboxClaim(
sql: DatabaseOperations,
table: OutboxTable,
claim: OutboxClaim,
failureTag: string
): Effect.Effect<void, SqlError>- Inputs:
sql: DatabaseOperationstable: OutboxTableclaim: OutboxClaimfailureTag: string
- Output:
Effect.Effect<void, SqlError> - Errors:
SqlError - Requirements: none
- Side effects: Writes the outbox row when the claim still owns it.
- Source: packages/database/src/outbox-lifecycle.ts:286
How it works
A worker that is interrupted before the provider answers releases its claim. Under the same
claim fence as markOutboxDelivered, it sets Pending with the failure tag and clears the
claim, so the next claim delivers the row. When the claim no longer owns the row, it succeeds
without a change, because stale recovery or another claim has already moved the row.
Use
releaseOutboxClaim(sql, invitationOutbox, claim, "InterruptedRecruitmentInvitationClaim");Avoid
Releasing a claim after the provider answered: the next claim sends the effect again.
Settle an answered delivery with markOutboxDelivered or markOutboxFailed.
Recovers every Processing row claimed before claimedBefore.
recoverStaleOutboxClaims(
sql: DatabaseOperations,
table: OutboxTable,
claimedBefore: string,
recovery: OutboxStaleRecovery
): Effect.Effect<number, SqlError>- Inputs:
sql: DatabaseOperationstable: OutboxTableclaimedBefore: stringrecovery: OutboxStaleRecovery
- Output:
Effect.Effect<number, SqlError> - Errors:
SqlError - Requirements: none
- Side effects: Writes every stale Processing row of the table.
- Source: packages/database/src/outbox-lifecycle.ts:321
How it works
One UPDATE moves each Processing row whose claimed_at lies before claimedBefore to
recovery.status, Pending or Failed, with recovery.failureTag, clears its claim, and
answers the number of rows it recovered. The worker of such a claim counts as gone; if it still
runs, its settlement fails with OutboxClaimLost.
Use
recoverStaleOutboxClaims(sql, invitationOutbox, claimedBefore, { status: "Failed", failureTag: "StaleClaimRecovered" });Avoid
A cutoff that a live claim can still reach, such as one closer to now than the longest
provider call: the row goes to a second claim, which delivers the effect again. Derive
claimedBefore from the worker's stale-claim window, as now minus staleClaimMilliseconds.
Builds the native SQL layer with an explicit host-owned connection limit.
nativePgLayer(
config: NativePgPoolConfig
): Layer.Layer<PgClient.PgClient | SqlClient.SqlClient, Schema.SchemaError | SqlError>- Inputs:
config: NativePgPoolConfig - Output:
Layer.Layer<PgClient.PgClient | SqlClient.SqlClient, Schema.SchemaError | SqlError> - Errors:
Schema.SchemaError | SqlError - Requirements: none
- Side effects: Opens native PostgreSQL sessions only after the layer runs.
- Source: packages/database/src/pg-pool.ts:72
How it works
The caller owns the layer scope. The native driver owns acquisition, transaction reservation, cancellation, and release. Each checkout is exclusive. The driver can queue waiting fibers, so this limit bounds connections, not admitted work. Prepared statements require an explicit choice for a compatible pooler. An absent connectionTTL preserves unlimited connection reuse. Building the layer performs no work. Its scope closes its native pool on every exit. EX-0034: native admission policy is patched at the upstream boundary. Omitted or false preserves the caller mask; explicit acquisitionInterruptible true interrupts only top-level admission. Explicit shutdownTimeout owns the graceful wait, then a 100ms real-close acknowledgement bound; no close event is a defect, not a successful disposal or transaction-outcome receipt.
Use
const sql = nativePgLayer({ url, maxConnections: 2, prepare: false, startupOptions: "-c search_path=public" });Avoid
Providing a new layer for each service or enabling multiplexing on a cancellable path.
Builds the supported Better Auth pool with its own host-supplied capacity.
AuthPoolLive(
config: { readonly postgresUrl: string },
options: AuthPoolOptions
): Layer.Layer<AuthPgPool, Schema.SchemaError>- Inputs:
config: { readonly postgresUrl: string }options: AuthPoolOptions
- Output:
Layer.Layer<AuthPgPool, Schema.SchemaError> - Errors:
Schema.SchemaError - Requirements: none
- Side effects: Opens supported pg sessions only when the auth adapter uses the layer.
- Source: packages/database/src/pg-pool.ts:123
How it works
This layer owns only the node-postgres pool. It never provides native SqlClient. The host provides it separately from Database. Native callback fibers close before the pool, and pool diagnostic fibers remain owned until the pool closes. A client deadline does not prove PostgreSQL cancellation or a committed outcome.
Use
const authPool = AuthPoolLive(config, {
maxConnections: 2,
applicationName: "vektorprogrammet-worker-auth",
pipeline: true,
startupOptions: "-c search_path=auth",
connectTimeoutMilliseconds: 2_000,
queryTimeoutMilliseconds: 1_000,
});Avoid
Providing this layer to a delivery-only event or sharing it with native Effect SQL.
Executes parsed statements sequentially on the provided native SQL client.
executePostgresMigration(
source: string
): Effect.Effect<void, SqlError | DatabaseMigrationExecutionError, SqlClient.SqlClient>- Inputs:
source: string - Output:
Effect.Effect<void, SqlError | DatabaseMigrationExecutionError, SqlClient.SqlClient> - Errors:
SqlError | DatabaseMigrationExecutionError - Requirements:
SqlClient.SqlClient - Side effects: Executes original statement bytes when the Effect runs.
- Source: packages/database/src/postgres-migration-statements.ts:84
How it works
The caller (normally Migrator) owns the transaction. Construction is suspended; this operation never commits, opens another pool, or retries. SQL errors remain typed. Interruption stops subsequent statements and the owner rolls back.
Use
yield* sql.withTransaction(executePostgresMigration(source));Avoid
Running it outside the migration owner's transaction or on a new pool.