Problem Statement. app/backend/prisma/schema.prisma declares ApiKey.key as a nullable
legacy plaintext column alongside keyHash and keyPreview. Code paths reading ApiKey.key
can still leak raw secrets if downstream services (e.g. an admin export job, an audit dump)
select that column. Continuing to model the legacy column in the canonical schema is also a
permanent regression risk: a future contributor refreshing the schema re-introduces plaintext
storage.
Why it matters. This is a humanitarian-aid PII-adjacent system whose security policy
outlaws storing raw credentials. A plain column also defeats hashing, rotation, and
keyPreview-based redacted logging.
Technical Context. prisma/schema.prisma has:
model ApiKey {
key String? @unique // legacy plaintext, retained “for migration”
keyHash String? @unique
keyPreview String?
…
}
The ApiKey model is consumed by src/api-keys/* and by src/common/guards/api-key.guard.ts.
Expected Outcome. The ApiKey.key column is dropped in a single migration after a one-time
rehash job. Schema validates that no controller or service reads ApiKey.key after the
migration lands.
Acceptance Criteria.
key removed from prisma/schema.prisma.
- A migration script hashes every remaining plaintext value and stores the resulting SHA-256 in
keyHash with a keyPreview of prefix…last4.
- A repository-wide grep proves no
prisma.apiKey.select({ key: … remains.
- A new test in
app/backend/test/api-keys/*.e2e-spec.ts covers rotation: revoke → reissue →
validate old key rejected, new key accepted.
Implementation Notes. Use prisma migrate to add a NOT NULL keyHash constraint with a
default empty string, backfill, then drop the column. Add a guard-level hash compare using
crypto.timingSafeEqual.
Files or modules likely to be affected. prisma/schema.prisma, prisma/migrations/*,
src/api-keys/*, src/common/guards/api-key.guard.ts,
app/backend/test/api-keys/* (new).
Dependencies. Issue #2 (legacy keystore).
Difficulty. Medium
Estimated effort. M
Problem Statement.
app/backend/prisma/schema.prismadeclaresApiKey.keyas a nullablelegacy plaintext column alongside
keyHashandkeyPreview. Code paths readingApiKey.keycan still leak raw secrets if downstream services (e.g. an admin export job, an audit dump)
select that column. Continuing to model the legacy column in the canonical schema is also a
permanent regression risk: a future contributor refreshing the schema re-introduces plaintext
storage.
Why it matters. This is a humanitarian-aid PII-adjacent system whose security policy
outlaws storing raw credentials. A plain column also defeats hashing, rotation, and
keyPreview-based redacted logging.
Technical Context.
prisma/schema.prismahas:The
ApiKeymodel is consumed bysrc/api-keys/*and bysrc/common/guards/api-key.guard.ts.Expected Outcome. The
ApiKey.keycolumn is dropped in a single migration after a one-timerehash job. Schema validates that no controller or service reads
ApiKey.keyafter themigration lands.
Acceptance Criteria.
keyremoved fromprisma/schema.prisma.keyHashwith akeyPreviewofprefix…last4.prisma.apiKey.select({ key: …remains.app/backend/test/api-keys/*.e2e-spec.tscovers rotation: revoke → reissue →validate old key rejected, new key accepted.
Implementation Notes. Use
prisma migrateto add aNOT NULL keyHashconstraint with adefault empty string, backfill, then drop the column. Add a guard-level hash compare using
crypto.timingSafeEqual.Files or modules likely to be affected.
prisma/schema.prisma,prisma/migrations/*,src/api-keys/*,src/common/guards/api-key.guard.ts,app/backend/test/api-keys/*(new).Dependencies. Issue #2 (legacy keystore).
Difficulty. Medium
Estimated effort. M