2.0 beta — new releases can include breaking changes. Stable 1.x docs
dimah-s3v2.0.0-beta.0
Database

Database Lifecycle Hooks

The guards and lifecycle hooks db() attaches to every tracked route.

The db() plugin attaches guards and lifecycle hooks to every route that does not set plugins: { db: false }. They keep storage_object in step with S3 and check that each call acts on rows of its own scope.

Hook order

Guards run instance, then route, then feature. Within each hook, the plugin runs first and your hook runs last. This holds for lifecycle on* hooks too, so your upload.onStart sees the pending row, and your upload.onConfirmed sees the row already active. See the plugin hook order.

Lifecycle by operation

HookWhat the plugin does
Upload ticketBinds the ticket to the caller's scope. A ticket from another scope is UPLOAD_TICKET_INVALID.
upload.guardChecks clientMetadata.folder, runs authorize with that folder, checks who owns the key, then checks the quota.
upload.onStartWrites the pending row with the upload's token, its confirm deadline, and its folder, then creates the folder and its ancestors when missing.
upload.confirmGuardAccepts only the row of this ticket's upload, owned by the caller, or a confirm it already finished (a retry).
upload.onConfirmedChecks the quota against the verified size, marks the row active with the size, type, and ETag from HeadObject, and retires the previousKey row.
upload.multipart.guardPart, list, and complete accept only the row of this ticket's upload. Abort checks the row only while it is still this upload's.
upload.multipart.onAbortRemoves the pending row, or ends the overwrite and keeps the previous object.
download.guardRequires an active row owned by the caller, then runs authorize with it.
delete.guardRequires a row owned by the caller (a soft-deleted one deletes again), then runs authorize with it.
delete.onDeletedAborts a multipart upload in flight, then soft- or hard-deletes the row (deleteMode).

onStart runs for presigned POST and PUT uploads, multipart init, and s3.put (kind: "server"). The row's expiresAt is the expiresAt of the onStart context: the upload ticket's expiry, or 15 minutes for s3.put. A pending row past that deadline stops counting toward the quota, and purge cleans it up. The deadline comes from the server's upload ticket settings, upload.confirmWindow and upload.multipart.ticketExpiresIn. There is no separate option.

Confirm replays

Confirm is stateless, and onConfirmed runs at least once. The plugin marks the row active with a compare-and-set on its status and upload token. A retried confirm or complete for an upload that is already confirmed returns the stored row, and the quota is not checked again. Your own onConfirmed can dedupe on uploadToken.

When the quota check at confirm fails, the plugin removes the row, and the server deletes the object and returns QUOTA_EXCEEDED.

Overwrites

An upload to a key that already has an active object fails with OBJECT_EXISTS (409), unless the route sets plugins: { db: { replace: "overwrite" } }. With it:

  • The active row stays active, with the previous object's metadata, until the new upload is confirmed. S3 replaces the bytes as soon as the new PUT lands, and a rejected confirm deletes the key.
  • The row keeps its folder, attributes, and createdAt. The overwrite's clientMetadata.folder is ignored.
  • An abort keeps the previous object. An expired overwrite is cleaned up by purge.

An upload to a key that purge is cleaning up gets CONFLICT. A retry after purge finishes goes through.

Errors

SituationStatusCode
resolveScope returns null401UNAUTHORIZED
An upload key belongs to another scope, or authorize says no403FORBIDDEN
The upload ticket was issued to another scope403UPLOAD_TICKET_INVALID
The row is missing, another scope's, or not this upload's404OBJECT_NOT_FOUND
An active object is at the key409OBJECT_EXISTS
Purge is cleaning the key, or a concurrent write won409CONFLICT
clientMetadata.folder is not a valid folder path400FOLDER_INVALID
The upload would exceed the quota403QUOTA_EXCEEDED

Route membership is still the server keyPrefix check (KEY_INVALID). The plugin looks rows up by (bucket, key) and compares the scope. It does not compare the stored route column to the request route. See route namespace vs ownership.

Frequently asked questions

On this page