Apply RLS context at task service boundaries, harden orchestrator/web integration and session startup behavior, re-enable targeted frontend tests, and lock vulnerable transitive dependencies so QA and security gates pass cleanly.
User Credentials Module
Secure storage and management of user API keys, OAuth tokens, and other credentials.
Overview
The User Credentials module provides encrypted storage for sensitive user credentials with:
- Encryption: OpenBao Transit (preferred) or AES-256-GCM fallback
- Access Control: Row-Level Security (RLS) with scope-based isolation
- Audit Logging: Automatic tracking of credential access and modifications
- Multi-tenancy: USER, WORKSPACE, and SYSTEM scope support
Database Model
UserCredential
| Field | Type | Description |
|---|---|---|
id |
UUID | Primary key |
userId |
UUID | Owner (FK to users) |
workspaceId |
UUID? | Optional workspace (FK to workspaces) |
name |
String | Display name (e.g., "GitHub Personal Token") |
provider |
String | Provider identifier (e.g., "github", "openai") |
type |
CredentialType | Type of credential (API_KEY, OAUTH_TOKEN, etc.) |
scope |
CredentialScope | Access scope (USER, WORKSPACE, SYSTEM) |
encryptedValue |
Text | Encrypted credential value |
maskedValue |
String? | Masked value for display (e.g., "****abcd") |
description |
Text? | Optional description |
expiresAt |
Timestamp? | Optional expiration date |
lastUsedAt |
Timestamp? | Last access timestamp |
metadata |
JSONB | Provider-specific metadata |
isActive |
Boolean | Soft delete flag (default: true) |
rotatedAt |
Timestamp? | Last rotation timestamp |
createdAt |
Timestamp | Creation timestamp |
updatedAt |
Timestamp | Last update timestamp |
Enums
CredentialType
API_KEY- API keys (GitHub, GitLab, etc.)OAUTH_TOKEN- OAuth tokensACCESS_TOKEN- Access tokensSECRET- Generic secretsPASSWORD- PasswordsCUSTOM- Custom credential types
CredentialScope
USER- Personal credentials (visible only to owner)WORKSPACE- Workspace-scoped credentials (visible to workspace admins)SYSTEM- System-level credentials (admin access only)
Security
Row-Level Security (RLS)
All credential access is enforced via PostgreSQL RLS policies:
-- USER scope: Owner access only
scope = 'USER' AND user_id = current_user_id()
-- WORKSPACE scope: Workspace admin access
scope = 'WORKSPACE' AND is_workspace_admin(workspace_id, current_user_id())
-- SYSTEM scope: Admin/migration bypass only
current_user_id() IS NULL
Encryption
Credentials are encrypted at rest using:
-
OpenBao Transit (preferred)
- Format:
vault:v1:base64data - Named key:
mosaic-credentials - Handled by
VaultService
- Format:
-
AES-256-GCM (fallback)
- Format:
iv:authTag:encrypted - Handled by
CryptoService - Uses
ENCRYPTION_KEYenvironment variable
- Format:
The encryption middleware automatically falls back to AES when OpenBao is unavailable.
Activity Logging
All credential operations are logged:
CREDENTIAL_CREATED- Credential createdCREDENTIAL_ACCESSED- Credential value decryptedCREDENTIAL_ROTATED- Credential value rotatedCREDENTIAL_REVOKED- Credential soft-deleted
Usage
Creating a Credential
const credential = await prisma.userCredential.create({
data: {
userId: currentUserId,
name: "GitHub Personal Token",
provider: "github",
type: CredentialType.API_KEY,
scope: CredentialScope.USER,
encryptedValue: await vaultService.encrypt(plainValue, TransitKey.CREDENTIALS),
maskedValue: maskValue(plainValue), // "****abcd"
metadata: { scopes: ["repo", "user"] },
},
});
Retrieving a Credential
// List credentials (encrypted values NOT returned)
const credentials = await prisma.userCredential.findMany({
where: { userId: currentUserId, scope: CredentialScope.USER },
select: {
id: true,
name: true,
provider: true,
type: true,
maskedValue: true,
lastUsedAt: true,
},
});
// Decrypt a specific credential (audit logged)
const credential = await prisma.userCredential.findUnique({
where: { id: credentialId },
});
const plainValue = await vaultService.decrypt(credential.encryptedValue, TransitKey.CREDENTIALS);
// Update lastUsedAt
await prisma.userCredential.update({
where: { id: credentialId },
data: { lastUsedAt: new Date() },
});
// Log access
await activityLog.log({
action: ActivityAction.CREDENTIAL_ACCESSED,
entityType: EntityType.CREDENTIAL,
entityId: credentialId,
});
Rotating a Credential
await prisma.userCredential.update({
where: { id: credentialId },
data: {
encryptedValue: await vaultService.encrypt(newPlainValue, TransitKey.CREDENTIALS),
maskedValue: maskValue(newPlainValue),
rotatedAt: new Date(),
},
});
Soft Deleting (Revoking)
await prisma.userCredential.update({
where: { id: credentialId },
data: { isActive: false },
});
API Endpoints (Planned - Issue #356)
POST /api/credentials Create credential
GET /api/credentials List credentials (masked values only)
GET /api/credentials/:id Get single credential (masked)
GET /api/credentials/:id/value Decrypt and return value (audit logged)
PATCH /api/credentials/:id Update metadata
POST /api/credentials/:id/rotate Rotate credential value
DELETE /api/credentials/:id Soft-delete (revoke)
Testing
Run tests with:
pnpm test apps/api/src/credentials/user-credential.model.spec.ts
Tests cover:
- Model structure and constraints
- Enum validation
- Unique constraints
- Foreign key relations
- Cascade deletes
- Timestamps
- JSONB metadata
Related Issues
- #355 - Create UserCredential Prisma model with RLS policies (this issue)
- #356 - Build credential CRUD API endpoints
- #353 - VaultService NestJS module for OpenBao Transit
- #350 - Add RLS policies to auth tables with FORCE enforcement
Migration
Migration: 20260207_add_user_credentials
Apply with:
cd apps/api
pnpm prisma migrate deploy
References
- Design doc:
docs/design/credential-security.md - OpenBao setup:
docs/OPENBAO.md - RLS context:
apps/api/src/lib/db-context.ts - CryptoService:
apps/api/src/federation/crypto.service.ts