{"_id":"@bjorntech/betterauth-dynamodb","_rev":"6-829376dd943d9cf7b8f713b5f439fa0e","name":"@bjorntech/betterauth-dynamodb","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@bjorntech/betterauth-dynamodb","version":"1.0.0","keywords":["better-auth","dynamodb","adapter","aws"],"license":"MIT","_id":"@bjorntech/betterauth-dynamodb@1.0.0","maintainers":[{"name":"bjorntechtobbe","email":"tobbe@bjorntech.com"},{"name":"marveltool","email":"carl@bjorntech.com"}],"homepage":"https://github.com/bjorntech/betterauth-dynamodb#readme","bugs":{"url":"https://github.com/bjorntech/betterauth-dynamodb/issues"},"dist":{"shasum":"f4593fe111ee47a4fe72a155fae28d718f97b1ba","tarball":"https://registry.npmjs.org/@bjorntech/betterauth-dynamodb/-/betterauth-dynamodb-1.0.0.tgz","fileCount":40,"integrity":"sha512-C2MkCo1c7pnkjBlaHVl9VT4lI/he9sSutY01qd6wOs4I1Vs0Mwfi1p4ZgYERKAFi9g9SHbl257QjtVow/ofTjg==","signatures":[{"sig":"MEUCIQDnoHExPqY5AbVDdNawmiTZw6ZjLZGQLyOHF8Tij9mcxQIgCynTLIlL0Gjrim5hEk6wTzdTwkGVo7IfZEQvFIai2xg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":134752},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"e93d6e5e64f4378e81de243e65ab163d9c2d51fb","scripts":{"lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","format":"prettier --write .","verify":"bun run typecheck && bun run lint && bun run test:coverage && bun run quality:crap && bun run build","prepack":"bun run build","typecheck":"tsc --noEmit","quality:crap":"bun scripts/crap-check.ts coverage/lcov.info src","test:coverage":"vitest run --coverage","smoke:dist-import":"bun run build && node scripts/dist-import-smoke.mjs","verify:integration":"bun run test:integration:local","test:integration:local":"vitest run --config vitest.integration.config.ts"},"_npmUser":{"name":"marveltool","email":"carl@bjorntech.com"},"repository":{"url":"git+https://github.com/bjorntech/betterauth-dynamodb.git","type":"git"},"_npmVersion":"11.12.1","description":"Production-oriented Better Auth DynamoDB adapter","directories":{},"_nodeVersion":"24.15.0","dependencies":{"@aws-sdk/lib-dynamodb":"^3.758.0","@aws-sdk/client-dynamodb":"^3.758.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.2.5","devDependencies":{"eslint":"^9.31.0","vitest":"^3.2.4","prettier":"^3.6.2","typescript":"^5.8.3","@types/node":"^20.0.0","better-auth":"^1.6.25","@babel/parser":"^7.28.0","testcontainers":"^12.0.4","@babel/traverse":"^7.28.0","typescript-eslint":"^8.38.0","@vitest/coverage-v8":"^3.2.4","@types/babel__traverse":"^7.20.7","@better-auth/test-utils":"1.6.25"},"peerDependencies":{"better-auth":"^1.6.25"},"_npmOperationalInternal":{"tmp":"tmp/betterauth-dynamodb_1.0.0_1785095251457_0.0042458447333999505","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@bjorntech/betterauth-dynamodb","version":"1.0.1","keywords":["better-auth","dynamodb","adapter","aws"],"license":"MIT","_id":"@bjorntech/betterauth-dynamodb@1.0.1","maintainers":[{"name":"bjorntechtobbe","email":"tobbe@bjorntech.com"},{"name":"marveltool","email":"carl@bjorntech.com"}],"homepage":"https://github.com/bjorntech/betterauth-dynamodb#readme","bugs":{"url":"https://github.com/bjorntech/betterauth-dynamodb/issues"},"dist":{"shasum":"202e2c92a568e0f51feb90de2ff7c6f1746980f2","tarball":"https://registry.npmjs.org/@bjorntech/betterauth-dynamodb/-/betterauth-dynamodb-1.0.1.tgz","fileCount":40,"integrity":"sha512-HFbbrQEtuiDV8sl16BW3NRUIQHVnGKZ6SWSrPe9MNZS2lbgbXxGPs0wLVyIu4VBhbdzyVY+/hGCGEXZkYAEXUQ==","signatures":[{"sig":"MEUCIFjGutiX5ifKY+nDvFfigoxwXf2Oe9HYkze5Ff2PwCWrAiEA6ED8eSEPv21VmrDrZuKzTcKe48/6oyP1CoUFnRrVquI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":135147},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"7b391f7dc0fbec0c45e1679dea32fc79cacea1e4","scripts":{"lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","format":"prettier --write .","verify":"bun run typecheck && bun run lint && bun run test:coverage && bun run quality:crap && bun run build","prepack":"bun run build","typecheck":"tsc --noEmit","quality:crap":"bun scripts/crap-check.ts coverage/lcov.info src","test:coverage":"vitest run --coverage","smoke:dist-import":"bun run build && node scripts/dist-import-smoke.mjs","verify:integration":"bun run test:integration:local","test:integration:local":"vitest run --config vitest.integration.config.ts"},"_npmUser":{"name":"marveltool","email":"carl@bjorntech.com"},"repository":{"url":"git+https://github.com/bjorntech/betterauth-dynamodb.git","type":"git"},"_npmVersion":"11.12.1","description":"Production-oriented Better Auth DynamoDB adapter","directories":{},"_nodeVersion":"24.15.0","dependencies":{"@aws-sdk/lib-dynamodb":"^3.758.0","@aws-sdk/client-dynamodb":"^3.758.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.2.5","devDependencies":{"eslint":"^9.31.0","vitest":"^3.2.4","prettier":"^3.6.2","typescript":"^5.8.3","@types/node":"^20.0.0","better-auth":"^1.6.25","@babel/parser":"^7.28.0","testcontainers":"^12.0.4","@babel/traverse":"^7.28.0","typescript-eslint":"^8.38.0","@vitest/coverage-v8":"^3.2.4","@types/babel__traverse":"^7.20.7","@better-auth/test-utils":"1.6.25"},"peerDependencies":{"better-auth":"^1.6.25"},"_npmOperationalInternal":{"tmp":"tmp/betterauth-dynamodb_1.0.1_1785096596956_0.03705108264601242","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"_id":"@bjorntech/betterauth-dynamodb@1.1.0","bugs":{"url":"https://github.com/bjorntech/betterauth-dynamodb/issues"},"dist":{"shasum":"1a107039af4662f617a42beac9052c504cbabaff","tarball":"https://registry.npmjs.org/@bjorntech/betterauth-dynamodb/-/betterauth-dynamodb-1.1.0.tgz","fileCount":40,"integrity":"sha512-3nt9He0CiXGbaQ2rqWp0aO8p1MrUyEYv3hASf6e2jgoGzBflYb4WCgJU2vhxrgpFW7PV8CD+xiYDNRnHWYCKkA==","signatures":[{"sig":"MEUCIQCn6NSXCVuIfnYjz9GE8yKGE2xCzkbXXusNOyONKkDoMQIgayAdy1cJ1sdY3vt8lh6pi8Oa2XNm8xMhT4Y7oFWHycg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBLWcTto1FXQDfBuirB6J93SFPPqBUfzOluYVMVIfON4AiEA7xwIm+7s+m4wUg2X42BffitvxXWeKJWfGgx8J57qGpE="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bjorntech%2fbetterauth-dynamodb@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":166427},"main":"./dist/index.js","name":"@bjorntech/betterauth-dynamodb","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"f9f3555b2f82432e1830d5c3299fc88280a1fee6","license":"MIT","scripts":{"lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","format":"prettier --write .","verify":"bun run typecheck && bun run lint && bun run test:coverage && bun run quality:crap && bun run build","prepack":"bun run build","typecheck":"tsc --noEmit","quality:crap":"bun scripts/crap-check.ts coverage/lcov.info src","test:coverage":"vitest run --coverage","smoke:dist-import":"bun run build && node scripts/dist-import-smoke.mjs","verify:integration":"bun run test:integration:local","test:integration:local":"vitest run --config vitest.integration.config.ts"},"version":"1.1.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:43eecec1-bf6c-4648-8404-a81232eb0281"}},"homepage":"https://github.com/bjorntech/betterauth-dynamodb#readme","keywords":["better-auth","dynamodb","adapter","aws"],"repository":{"url":"git+https://github.com/bjorntech/betterauth-dynamodb.git","type":"git"},"_npmVersion":"12.0.2","description":"Production-oriented Better Auth DynamoDB adapter","directories":{},"maintainers":[{"name":"bjorntechtobbe","email":"tobbe@bjorntech.com"},{"name":"marveltool","email":"carl@bjorntech.com"}],"_nodeVersion":"22.23.2","dependencies":{"@aws-sdk/lib-dynamodb":"^3.758.0","@aws-sdk/client-dynamodb":"^3.758.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.2.5","devDependencies":{"eslint":"^9.31.0","vitest":"^3.2.4","prettier":"^3.6.2","typescript":"^5.8.3","@types/node":"^20.0.0","better-auth":"1.7.5","@babel/parser":"^7.28.0","testcontainers":"^12.0.4","@babel/traverse":"^7.28.0","@better-auth/core":"1.7.5","typescript-eslint":"^8.38.0","@vitest/coverage-v8":"^3.2.4","@types/babel__traverse":"^7.20.7","@better-auth/test-utils":"1.7.5","@better-auth/oauth-provider":"1.7.5"},"peerDependencies":{"better-auth":"^1.6.25"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/betterauth-dynamodb_1.1.0_1789830733021_0.45357608326798005"}}},"time":{"created":"2026-07-26T11:17:05.395Z","modified":"2026-09-19T15:12:13.547Z","0.1.0":"2026-07-26T11:17:05.907Z","1.0.0":"2026-07-26T19:47:31.601Z","1.0.1":"2026-07-26T20:09:57.148Z","1.1.0":"2026-09-19T15:12:13.127Z"},"bugs":{"url":"https://github.com/bjorntech/betterauth-dynamodb/issues"},"license":"MIT","homepage":"https://github.com/bjorntech/betterauth-dynamodb#readme","keywords":["better-auth","dynamodb","adapter","aws"],"repository":{"url":"git+https://github.com/bjorntech/betterauth-dynamodb.git","type":"git"},"description":"Production-oriented Better Auth DynamoDB adapter","maintainers":[{"name":"bjorntechtobbe","email":"tobbe@bjorntech.com"},{"name":"marveltool","email":"carl@bjorntech.com"}],"readme":"# @bjorntech/betterauth-dynamodb\n\nProduction-oriented DynamoDB adapter targeting Better Auth `^1.6.25` via the official `createAdapterFactory` API.\n\n## Status\n\nThis package is published on npm as `@bjorntech/betterauth-dynamodb`. The repository has unit coverage for command construction, adapter semantics, query planning, and Better Auth factory wiring, plus explicit Docker-backed DynamoDB Local integration suites (including a separate Better Auth adapter conformance test file).\n\nStorage compatibility note: the current storage format uses transactionally maintained scalar equality sidecar rows in the base table, delimiter-safe length-prefixed key components, SHA-256 hashes for sidecar/unique-lock values, and hidden internal revision metadata for ABA-safe mutations. Experimental tables using older generic `gsi1`/`idx_<field>_*`, pre-length-prefixed, pre-hash, or pre-revision formats should be recreated or migrated before using this version; pre-revision rows can still be read but fail clearly if mutated.\n\nUpgrade note: 1.0.1 to 1.1.0 requires no migration when `enforceSchemaUniqueIndexes` remains at its default `false`, and `IN` support requires no migration. For existing records, opting into new schema unique constraints requires a stopped-write audit, duplicate repair, and application-owned lock backfill before all compatible writers restart with enforcement enabled. New empty affected models need no backfill. No migration utility is supplied.\n\n## Installation\n\nInstall the adapter and its Better Auth peer dependency with your package manager:\n\n```sh\nnpm install @bjorntech/betterauth-dynamodb better-auth\nbun add @bjorntech/betterauth-dynamodb better-auth\npnpm add @bjorntech/betterauth-dynamodb better-auth\n```\n\nAWS SDK DynamoDB packages are runtime dependencies of this adapter.\n\n## Basic usage\n\nPrefer injecting a `DynamoDBDocumentClient` so your application owns AWS configuration, credentials, middleware, tracing, and marshalling behavior:\n\n```ts\nimport { DynamoDBClient } from \"@aws-sdk/client-dynamodb\";\nimport { DynamoDBDocumentClient } from \"@aws-sdk/lib-dynamodb\";\nimport { betterAuth } from \"better-auth\";\nimport { dynamoDBAdapter } from \"@bjorntech/betterauth-dynamodb\";\n\nconst dynamo = DynamoDBDocumentClient.from(new DynamoDBClient({}), {\n  marshallOptions: { removeUndefinedValues: true }\n});\n\nexport const auth = betterAuth({\n  database: dynamoDBAdapter({\n    tableName: \"better-auth\",\n    client: dynamo,\n    ttl: { fields: { session: \"expiresAt\", verification: \"expiresAt\" } }\n  }),\n  verification: { disableCleanup: true },\n  emailAndPassword: { enabled: true }\n});\n```\n\nWhen the `verification` model uses adapter-managed TTL, disable Better Auth's verification cleanup as shown above. Otherwise Better Auth can issue a range-only `deleteMany` such as `expiresAt < now`, which this adapter rejects by default because it would require a hidden table/model scan.\n\nFor local development you may pass `region`, `endpoint`, and/or `dynamoDBClientConfig` instead of `client`. Production deployments should usually inject the client.\n\nSee [examples](./examples/) for standalone deployment examples that use local path dependencies.\n\n## API\n\n```ts\nimport { dynamoDBAdapter } from \"@bjorntech/betterauth-dynamodb\";\n```\n\n`dynamoDBAdapter(options)` accepts:\n\n- `tableName: string` - required DynamoDB table name.\n- `client?: DynamoDBDocumentClient` - preferred production integration path.\n- `region?: string`, `endpoint?: string`, `dynamoDBClientConfig?: DynamoDBClientConfig` - used only when this package creates the AWS client.\n- `ttl?: false | { attributeName?: string; fields?: Record<string, string>; defaultField?: string }` - derives a DynamoDB TTL epoch-seconds attribute from Better Auth date fields. When omitted or `false`, logical TTL filtering is inactive and a user/plugin field named `ttl` remains ordinary visible data.\n- `unsafeAllowScan?: boolean` - defaults to `false`; must be explicitly enabled for scan-shaped query/update/delete/count operations.\n- `maxPages?: number` - positive safe integer maximum DynamoDB Query/Scan pages drained for sidecar equality lookups and explicit unsafe scans; defaults to `25`. If DynamoDB still has more pages at this cap, the adapter throws rather than returning incomplete filtered/windowed results.\n- `pageSize?: number` - optional positive safe integer DynamoDB `Limit` for each Query/Scan request. Pagination is still fully drained up to `maxPages`, so this controls request sizing and does not change the final logical result. It is mainly useful for testing pagination and tightly bounded local workloads; leave unset in normal production use unless you have measured the throughput/latency trade-off.\n- `uniqueFields?: Record<string, string[]>` - optional manual unique-field configuration merged with Better Auth schema unique fields. Keys and fields must use the exact DynamoDB/storage names the adapter receives after Better Auth `modelName`/`fieldName` mapping (for example `{ app_user: [\"email_address\"] }`, not `{ user: [\"email\"] }`, when those mappings are configured). Each listed field is an independent single-field unique constraint; composite uniqueness is not supported by this option.\n- `enforceSchemaUniqueIndexes?: boolean` - opt-in enforcement of unique Better Auth schema `indexes`; defaults to `false`. Unique index fields follow Better Auth's own rules: 1–16 distinct required fields, excluding `json`, `string[]`, and `number[]` types (enum-literal string types are allowed). Existing rows are not backfilled automatically and pre-existing records receive no retroactive locks; activate only after an application-owned duplicate audit and compatible lock backfill.\n- `maxBulkConcurrency?: number` - positive safe integer concurrency cap for independent `updateMany`/`deleteMany` transactions; defaults to `8`. A failure can follow successful mutations, so bulk operations are not aggregate-atomic and must not be blindly retried.\n\nThe schema-index option consumes Better Auth's actual table-level `indexes` array (`DBTableIndex[]`) and only acts on entries with `unique: true`; it does not add an alternative schema-definition option. On a new empty table it can be enabled directly. For a populated table, use a maintenance window with writes stopped: audit duplicates, repair them, and complete an application-owned lock backfill before restarting all writers with compatible code and enforcement enabled. This package provides no migration or backfill utility; do not enable it on unbackfilled rows or allow old/incompatible writers to continue. The `UNIQUE2` namespace is stable only while the index name (explicit or derived), ordered field list, and model/field mapping remain unchanged; changing any of those requires backfilling the new namespace, and pre-existing records do not acquire locks automatically. Rollback requires an explicit plan for versioned locks and writers that may have observed the constraint; disabling the option alone does not undo backfill or repair duplicates.\n\nThe package also exports `BetterAuthDynamoDBOptions`, `TtlOptions`, `DynamoDBAdapterError`, and `UnsupportedQueryError`.\n\n## Deployment notes\n\nKeep table ownership in your application infrastructure and inject a `DynamoDBDocumentClient` into the adapter. Provision a DynamoDB table with `pk` as the partition key, `sk` as the sort key, and DynamoDB TTL enabled on the adapter TTL attribute when you configure adapter-managed TTL. No GSIs are required for the current sidecar-index design.\n\nFor email OTP sign-in or other flows where verification rows expire through DynamoDB TTL, keep `verification.disableCleanup: true` in the Better Auth config. Without it, Better Auth attempts to clean verification rows with a range-only `deleteMany(expiresAt < now)`, and the adapter rejects that no-key access pattern under its no-hidden-scan policy.\n\nBetter Auth database-backed rate limiting has the same DynamoDB trade-off. The rate-limit `key` field is schema-unique and works well for exact-key increments, but Better Auth cleanup can require range-only predicates such as old `lastRequest` values. Leave `unsafeAllowScan` disabled for production unless you have a tightly bounded table and have accepted the cost/consistency profile. Prefer Better Auth's non-database/in-memory limiter for single-instance local development, an edge/API-gateway/WAF limiter, or an application-owned DynamoDB rate-limit table with access patterns designed for your cleanup needs.\n\nIf a needed access pattern has no id or scalar equality predicate, the adapter throws unless `unsafeAllowScan: true` is set. `OR` predicates and `mode: \"insensitive\"` equality also cannot use keyed access safely: `OR` can match rows outside one narrowed key branch, and DynamoDB entity/sidecar keys are case-sensitive. Those shapes require `unsafeAllowScan: true` so the adapter can explicitly scan the bounded model partition and evaluate the full expression in memory. A future optional native-GSI optimization may be added with descriptive API names, but there is no generic GSI option today.\n\n## Table and key model\n\n- Table primary key: `pk = MODEL#s<byteLength>:<model>`, `sk = ID#s<byteLength>:<id>`. Model, field, and id components use deterministic length prefixes so embedded delimiters such as `#` cannot collide across entity, sidecar, or unique-lock rows.\n- The Better Auth-visible record is stored in `entity` and scalar fields are duplicated at top level for conditional checks and scan-opt-in filtering.\n- Before writes, `undefined` values are removed from plain object records and arrays because DynamoDB has no undefined value type. Array entries containing `undefined` are omitted, which compacts arrays. Non-plain values such as `Date`, binary values, sets, and class instances are preserved unchanged; if a preserved custom instance is not marshalable by your injected AWS SDK client, normalize it to a plain supported value before passing it to Better Auth.\n- Scalar equality sidecars are stored in the same table: `pk = INDEX#s<byteLength>:<model>#FIELD#s<byteLength>:<field>#VALUE#<sha256(typed-value)>`, `sk = OWNER#s<byteLength>:<id>`. The typed value encoding (before hashing) preserves distinctions such as string `\"1\"`, number `1`, boolean `true`, dates, and `null`, and supports arbitrary Better Auth/plugin scalar fields without one GSI per field. Owner rows are always re-read and checked against the original typed predicate, so the hash is only a bounded DynamoDB key component.\n- Unique fields declared by Better Auth schema, or by `uniqueFields`, create transactional lock rows: `pk = UNIQUE#s<byteLength>:<model>#s<byteLength>:<field>`, `sk = VALUE#<sha256(typed-value)>#s0:`.\n- When `enforceSchemaUniqueIndexes` is enabled, each unique Better Auth schema index adds a `UNIQUE2` compound lock row (a storage-format addition; existing entity, scalar-sidecar, and single-field unique-lock rows are unchanged): `pk = UNIQUE2#s<byteLength>:<model>#s<byteLength>:<index-namespace>`, `sk = TUPLE#<sha256(type-prefixed tuple)>`. Components use delimiter-safe length prefixes, and tuple values are type-prefixed before hashing. An explicit index `name` is the namespace. Without one, the namespace is the ordered, model-remapped field list encoded as `s<UTF-8 byte length>:<field>` components joined by `#`, then length-prefixed again by `compoundUniquePk`; declared field order is preserved. For example, fields `email`, `tenantId` and values `\"alice@example.com\"`, `\"acme\"` produce `pk = UNIQUE2#s4:user#s20:s5:email#s8:tenantId` and `sk = TUPLE#7a2e1d8fc9815968b003de5dfc1ce4e013de254c811f4566bfaef8dc444963fa`. A `null` or `undefined` tuple component skips the lock; an empty string is a real value. Renaming an index or changing its field list or order changes its namespace, so existing locks must be re-backfilled; pre-existing records receive no retroactive locks.\n- Do not bypass this adapter with direct DynamoDB writes for Better Auth records. Entity rows, scalar sidecars, unique locks, TTL attributes, and hidden revision metadata must be created, replaced, and deleted together; out-of-band writes can break uniqueness, query results, logical expiration, or optimistic mutation guards.\n- Entity rows include a reserved top-level `__betterAuthDynamoDBRevision` UUID. It is generated on create and refreshed on each replacement/update/increment, used only for optimistic write guards, and never returned as Better Auth-visible metadata.\n- DynamoDB partition keys are validated against the 2048-byte UTF-8 limit and sort keys against the 1024-byte UTF-8 limit. Long scalar field values are hashed in sidecar and unique-lock keys to avoid partition/sort key overflow; long model, field, or id components fail with an actionable error.\n- TTL is derived from configured date fields into a DynamoDB TTL attribute (default `ttl`) and is not added to the Better Auth-visible `entity`. Entity, scalar sidecar, and unique-lock rows receive compatible TTL metadata when available. Reads treat configured TTL as logical expiration: an owner row at or before the current epoch second is hidden for id, sidecar, and explicit scan paths even if DynamoDB has not physically deleted it. Sidecars/locks may therefore expire with their owner; stale sidecars still cannot be returned because the owner entity must exist, be unexpired, and match the requested typed value. When `ttl` is omitted or `false`, this logical expiration path is completely disabled; use `attributeName` if you configure TTL and also need a user/plugin field named `ttl` at top level.\n- Expired compound locks are logically ignored for reads, but reuse can still wait for DynamoDB's physical TTL deletion. Do not treat logical expiration as immediate physical lock removal.\n\n## Concurrency guarantees and limitations\n\n- `create` transactionally writes the entity row, scalar equality sidecars, and configured uniqueness locks with conditional non-existence checks.\n- `update`, `updateMany`, `delete`, and `deleteMany` transactionally maintain sidecars and uniqueness locks. After the adapter reads and matches a target, the write uses an optimistic condition on the hidden per-entity revision rather than trying to translate arbitrary Better Auth operators into DynamoDB transaction conditions. This rejects stale ABA-shaped writes even if record contents changed away and back between read and mutation.\n- `consumeOne` transactionally deletes the matched entity, sidecars, and uniqueness locks with the same revision guard. Exactly one concurrent caller can consume a row; normal stale/lost consume races resolve to `null` as Better Auth expects, while non-conditional AWS failures still throw.\n- `incrementOne` uses a DynamoDB transactional `Update` with native numeric `ADD` for numeric/missing counter attributes plus the same revision guard, while maintaining scalar sidecars and uniqueness locks in the same `TransactWriteItems` request. Signed and zero finite deltas are supported; existing non-number counter values follow Better Auth's fallback semantics and are treated as zero with a guarded `SET` instead of invalid DynamoDB `ADD`. The returned row is the updated Better Auth-visible entity computed from the guarded snapshot; if the target guard no longer matches, the operation returns `null`. Unique-lock conflicts or non-conditional AWS failures still throw.\n- Transactional writes include a fresh AWS `ClientRequestToken` per command construction. This gives AWS SDK/internal network retries of that single send a stable token, but it is not cross-invocation or application-level idempotency.\n- DynamoDB `TransactWriteItems` is limited to 100 actions. The adapter de-duplicates configured unique-field lists, validates that a transaction contains at most one action per item key, rejects serialized transaction requests over approximately 4 MB, and fails early with an actionable error if entity + sidecars + unique locks for a mutation would exceed either limit.\n- Better Auth callback transactions are unsupported because DynamoDB has no interactive transaction API; the adapter reports `transaction: false`. DynamoDB `TransactWriteItems` is single-shot and is used internally for single-record atomic operations.\n- Queries without `id` equality or scalar equality require `unsafeAllowScan: true`; this avoids accidental full-table/model draining. Queries containing `OR` predicates, or equality clauses with `mode: \"insensitive\"` and no other safe keyed equality, also require explicit unsafe scans. `updateMany`, `deleteMany`, and `count` obey the same guard.\n- Base entity gets, sidecar queries, owner gets, and explicit unsafe scans request `ConsistentRead: true`. Strongly consistent reads cost more RCUs than eventually consistent reads. DynamoDB still does not provide a whole-operation snapshot for a paginated scan, so explicit unsafe scans can observe changes between pages.\n- Scalar equality queries use paginated base-table `Query` calls on the sidecar partition, then retrieve owner entity rows and apply residual `where`, sorting, offset, limit, and count in memory. Explicit unsafe scans are also paginated before in-memory filtering/windowing. Keep high-cardinality equality partitions and scan-enabled workloads bounded, and tune `maxPages` for your data shape; if the page cap is reached before `LastEvaluatedKey` clears, the adapter throws instead of returning a partial result. `pageSize` can force smaller Query/Scan pages when you intentionally want more, smaller requests.\n- Scalar `IN` predicates on `id` or a scalar field use one keyed lookup per distinct candidate value, issued sequentially so the shared page budget stays deterministic; a 1,000-value `IN` therefore costs roughly 1,000 sequential DynamoDB round-trips before owner reads. Keep `IN` lists small in latency-sensitive paths. Empty lists are no-ops; duplicate values are removed; more than 1,000 distinct values are rejected before reads. Sidecar queries share one global `maxPages` budget, owner reads are bounded by `maxBulkConcurrency`, and residual filters run before global sorting, offset, and limit windowing. Case-insensitive `IN` cannot use exact-case indexes as a complete access path: it requires an independent safe equality anchor or explicit `unsafeAllowScan: true`; with an anchor it is evaluated as a residual predicate. Case-insensitive `IN`/`not_in` comparisons normalize array members.\n- `updateMany` and `deleteMany` use bounded concurrency (controlled by `maxBulkConcurrency`, default `8`) and one revision-protected transaction per matched record. On failure they stop claiming new records and wait for started workers; earlier commits are not rolled back. They return numeric affected counts and are not aggregate-atomic or safe for blind retries. Each transaction has a 100-action and approximate JSON/practical byte-size preflight; AWS remains the ultimate size authority.\n- Joins use Better Auth's fallback behavior by default: Better Auth performs separate `findOne`/`findMany` calls, which remain subject to this adapter's no-hidden-scan policy. Better Auth's native-join path (`advanced.database.joins` on 1.7.x, `experimental.joins` on 1.6.x) is not supported because DynamoDB cannot honestly join arbitrary Better Auth models without explicit access-pattern design; if enabled and Better Auth passes a native join to the adapter, the adapter throws `UnsupportedQueryError` with instructions to disable the option.\n- The local integration suite uses AWS's official `amazon/dynamodb-local:2.6.1` image through Testcontainers. It proves command behavior against a real DynamoDB API surface for table keys, conditional transactions, sidecar/unique-lock persistence, logical TTL filtering, pagination tokens, and injected clients. The separate `test/integration/betterauth-conformance.test.ts` also runs Better Auth's adapter conformance suites and enables `unsafeAllowScan` because the canonical suites intentionally exercise scan-shaped predicates; production defaults still reject those shapes unless explicitly opted in. These tests do not prove IAM, global tables, streams, PITR/backups, encryption, adaptive capacity, throttling behavior, CloudWatch metrics, real service TTL deletion timing, or every production transaction-conflict edge; validate those in AWS for production-critical rollouts.\n\n## Verification, coverage, and CRAP policy\n\n```sh\nbun run verify\nbun run test:integration:local\n```\n\n`bun run verify` runs typecheck, ESLint, coverage tests, CRAP check, and build. The package is tested against Better Auth 1.7.5 and requires Better Auth `^1.6.25` as a peer dependency. `bun run test:integration:local` starts DynamoDB Local in Docker using a random mapped port, fake credentials, in-memory shared DB mode, telemetry disabled, and isolated tables for the local integration and separate adapter conformance suites.\n\nProvider coverage uses the published `@better-auth/oauth-provider` 1.7.5 plugin schema and direct query-shape tests, plus ordinary Better Auth HTTP authentication flows. It does not include the provider's full OAuth endpoint surface.\n\nCoverage thresholds are enforced for production `src` code (excluding pure types). CRAP is computed per production function as:\n\n```txt\nCRAP = complexity^2 * (1 - lineCoverage)^3 + complexity\n```\n\nThe verification command fails if any implementation function has CRAP `> 6`. Do not lower coverage thresholds or exclude production implementation files to bypass this gate.\n\n## Scripts\n\n- `bun run typecheck`\n- `bun run lint`\n- `bun run test`\n- `bun run test:coverage`\n- `bun run test:integration:local`\n- `bun run quality:crap`\n- `bun run build`\n- `bun run smoke:dist-import`\n- `bun run verify`\n- `bun run verify:integration`\n\n## Contributing\n\nSee [CONTRIBUTING.md](./CONTRIBUTING.md). Contributions should preserve atomic correctness, avoid hidden scans, maintain injected-client compatibility, and keep the CRAP <= 6 gate passing.\n\n## License\n\nMIT © 2026 BjornTech AB. See [LICENSE](./LICENSE).\n","readmeFilename":"README.md"}