From 0ed44cba0a98b878312015d60ae34e4401feae31 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Fri, 21 Nov 2025 20:59:33 +0000 Subject: [PATCH 01/23] secret storage --- Dockerfile | 3 +- SECRET_STORAGE_SPECIFICATION.md | 1809 +++++++++++++++++ backend/Dockerfile | 33 - backend/src/app.module.ts | 2 + .../src/decorators/company-id.decorator.ts | 23 + .../secret-access-log.entity.ts | 55 + .../application/dto/audit-log.dto.ts | 44 + .../application/dto/create-secret.dto.ts | 40 + .../application/dto/found-secret.dto.ts | 30 + .../application/dto/secret-list.dto.ts | 40 + .../application/dto/update-secret.dto.ts | 16 + .../user-secret/user-secret.controller.ts | 171 ++ .../user-secret/user-secret.entity.ts | 56 + .../user-secret/user-secret.module.ts | 15 + .../user-secret/user-secrets.service.spec.ts | 280 +++ .../user-secret/user-secrets.service.ts | 324 +++ .../1763724061000-CreateUserSecretEntity.ts | 38 + ...63724062000-CreateSecretAccessLogEntity.ts | 40 + .../non-saas-secrets-e2e.test.ts | 659 ++++++ docker-compose.tst.yml | 12 +- justfile | 2 +- 21 files changed, 3649 insertions(+), 43 deletions(-) create mode 100644 SECRET_STORAGE_SPECIFICATION.md delete mode 100644 backend/Dockerfile create mode 100644 backend/src/decorators/company-id.decorator.ts create mode 100644 backend/src/entities/secret-access-log/secret-access-log.entity.ts create mode 100644 backend/src/entities/user-secret/application/dto/audit-log.dto.ts create mode 100644 backend/src/entities/user-secret/application/dto/create-secret.dto.ts create mode 100644 backend/src/entities/user-secret/application/dto/found-secret.dto.ts create mode 100644 backend/src/entities/user-secret/application/dto/secret-list.dto.ts create mode 100644 backend/src/entities/user-secret/application/dto/update-secret.dto.ts create mode 100644 backend/src/entities/user-secret/user-secret.controller.ts create mode 100644 backend/src/entities/user-secret/user-secret.entity.ts create mode 100644 backend/src/entities/user-secret/user-secret.module.ts create mode 100644 backend/src/entities/user-secret/user-secrets.service.spec.ts create mode 100644 backend/src/entities/user-secret/user-secrets.service.ts create mode 100644 backend/src/migrations/1763724061000-CreateUserSecretEntity.ts create mode 100644 backend/src/migrations/1763724062000-CreateSecretAccessLogEntity.ts create mode 100644 backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts diff --git a/Dockerfile b/Dockerfile index 1eb4e8775..b7e3186c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,8 +45,7 @@ RUN cd shared-code && ../node_modules/.bin/tsc RUN cd backend && yarn run nest build COPY --from=front_builder /app/frontend/dist/dissendium-v0 /var/www/html COPY frontend/nginx/default.conf /etc/nginx/sites-enabled/default - -RUN chown -R appuser:appuser /app +RUN mkdir -p /app/backend/node_modules/.cache && chown -R appuser:appuser /app/backend/node_modules/.cache RUN chown -R appuser:appuser /var/lib/nginx RUN chown -R appuser:appuser /var/log/nginx RUN chown -R appuser:appuser /run diff --git a/SECRET_STORAGE_SPECIFICATION.md b/SECRET_STORAGE_SPECIFICATION.md new file mode 100644 index 000000000..8c20b7218 --- /dev/null +++ b/SECRET_STORAGE_SPECIFICATION.md @@ -0,0 +1,1809 @@ +# Secret Storage Feature Specification + +**Version:** 1.0 +**Date:** 2025-11-21 +**Status:** Draft + +## Table of Contents +1. [Overview](#overview) +2. [Goals and Objectives](#goals-and-objectives) +3. [Functional Requirements](#functional-requirements) +4. [Technical Design](#technical-design) +5. [Database Schema](#database-schema) +6. [API Specification](#api-specification) +7. [Security Design](#security-design) +8. [Frontend Requirements](#frontend-requirements) +9. [Implementation Phases](#implementation-phases) +10. [Testing Strategy](#testing-strategy) +11. [Migration and Rollout](#migration-and-rollout) +12. [Future Enhancements](#future-enhancements) + +--- + +## Overview + +### Context +RocketAdmin is a multi-database administration panel that currently stores encrypted database connection credentials. Users need a simple way to store and manage other sensitive information (API keys, tokens, certificates, passwords) related to their company's database work. + +### Problem Statement +Users currently have no secure way to: +- Store API keys for external services they use with their databases +- Manage authentication tokens needed for integrations +- Store certificates and encryption keys +- Audit access to sensitive information + +### Proposed Solution +Implement a simple, encrypted secret storage system that allows users to securely store, manage, and audit access to sensitive company information, leveraging RocketAdmin's existing encryption infrastructure. + +--- + +## Goals and Objectives + +### Primary Goals +1. **Secure Storage**: Provide military-grade encryption for company secrets +2. **User Experience**: Make secret management intuitive and seamless +3. **Access Control**: Company-based access only +4. **Audit Trail**: Track all access and modifications to secrets +5. **Integration**: Leverage existing encryption and authentication infrastructure + +### Success Criteria +- Users can create, read, update, and delete company secrets +- Secrets are encrypted at rest using existing infrastructure +- Master password protection available (optional) +- All secret access is logged +- Frontend provides intuitive UI similar to connection management +- Zero data breaches or unauthorized access incidents + +### Non-Goals (Out of Scope for v1) +- External vault integration (AWS Secrets Manager, HashiCorp Vault) +- Secret rotation automation +- Secret generators +- Secret versioning with full history +- Secret templates +- Bulk import/export +- User-to-user secret sharing +- Tags and categorization +- Secret types/categories + +--- + +## Functional Requirements + +### FR-1: Secret CRUD Operations + +#### FR-1.1: Create Secret +- **Actor**: Authenticated user +- **Preconditions**: User is logged in and belongs to a company +- **Flow**: + 1. User navigates to secrets page + 2. User clicks "Add Secret" + 3. User fills in secret details (slug, value) + 4. User optionally enables master password protection + 5. System validates input + 6. System encrypts secret + 7. System saves secret to database linked to user's company + 8. System logs creation event +- **Postconditions**: Secret is stored encrypted in database and associated with company +- **Validations**: + - Slug: 1-255 characters, letters (uppercase/lowercase), numbers, hyphens, underscores only, unique per company, required + - Value: 1-10000 characters, required + - Master password: If enabled, 8+ characters + +#### FR-1.2: Read Secret +- **Actor**: Authenticated user in the same company +- **Preconditions**: User belongs to the same company as the secret +- **Flow**: + 1. User navigates to secrets page + 2. User sees list of company secrets (slug only) + 3. User clicks on secret to view details + 4. If master password protected, system prompts for master password + 5. System validates user is in same company + 6. System decrypts secret + 7. System displays secret value (initially masked) + 8. User clicks "reveal" to show value + 9. System logs access event +- **Postconditions**: Secret access is logged +- **Security**: + - Secret value initially masked (****) + - Master password required if enabled + - Access logged with timestamp, IP, user agent + +#### FR-1.3: Update Secret +- **Actor**: Authenticated user (company member) +- **Preconditions**: User belongs to the same company as the secret +- **Flow**: + 1. User opens secret details + 2. User clicks "Edit" + 3. User modifies fields (can change value only, slug is immutable) + 4. User saves changes + 5. System validates input + 6. System re-encrypts secret (if value changed) + 7. System updates database + 8. System logs update event +- **Postconditions**: Secret is updated, audit log created +- **Validations**: Value: 1-10000 characters, required + +#### FR-1.4: Delete Secret +- **Actor**: Authenticated user (company member) +- **Preconditions**: User belongs to the same company as the secret +- **Flow**: + 1. User opens secret details + 2. User clicks "Delete" + 3. System prompts for confirmation + 4. User confirms deletion + 5. System permanently deletes secret from database + 6. System logs deletion event (before deletion) +- **Postconditions**: Secret is permanently deleted, audit log preserved +- **Security**: Any company member can delete company secrets + +### FR-2: Search Secrets + +#### FR-2.1: Search Secrets +- **Actor**: Authenticated user +- **Preconditions**: User is logged in +- **Flow**: + 1. User enters search query + 2. System searches slug only + 3. System returns matching secrets from user's company +- **Postconditions**: Filtered list displayed + +### FR-3: Secret Expiration + +#### FR-3.1: Set Secret Expiration +- **Actor**: Secret creator +- **Preconditions**: User created the secret +- **Flow**: + 1. User opens secret details + 2. User enables expiration + 3. User sets expiration date + 4. System saves expiration date +- **Postconditions**: Secret has expiration date +- **Validations**: Date must be in future + +#### FR-3.2: Handle Expired Secrets +- **Actor**: System (background job) +- **Preconditions**: N/A +- **Flow**: + 1. System runs daily job + 2. System finds secrets with expires_at < now + 3. System marks secrets as expired + 4. System sends notification to creator +- **Postconditions**: Expired secrets cannot be accessed +- **Behavior**: Expired secrets cannot be viewed until creator extends expiration + +### FR-4: Audit Logging + +#### FR-4.1: Log All Access +- **Actor**: System +- **Preconditions**: Any secret operation occurs +- **Flow**: + 1. User performs action (view, copy, update, delete) + 2. System captures event details + 3. System writes to audit log +- **Postconditions**: Audit record created +- **Logged Data**: + - Timestamp + - User ID + - Secret ID + - Action type + - IP address + - User agent + - Success/failure + +#### FR-4.2: View Audit Log +- **Actor**: Authenticated user (company member) +- **Preconditions**: User belongs to the same company as the secret +- **Flow**: + 1. User opens secret details + 2. User clicks "Audit Log" tab + 3. System displays access history +- **Postconditions**: User sees who accessed secret and when + +--- + +## Technical Design + +### Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Frontend (Angular) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Secrets List │ │Secret Details│ │ Share Dialog │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ HTTPS/JWT +┌─────────────────────────────────────────────────────────────┐ +│ Backend (NestJS) │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Secrets Module │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │ │ +│ │ │ Controller │ │ Service │ │Repository │ │ │ +│ │ └──────────────┘ └──────────────┘ └───────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Encryptor Service (Existing) │ │ +│ │ - encryptData() │ │ +│ │ - decryptData() │ │ +│ │ - encryptDataMasterPwd() │ │ +│ │ - decryptDataMasterPwd() │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Authorization Guards (Existing) │ │ +│ │ - JwtAuthGuard │ │ +│ │ - SecretAccessGuard (New) │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────────┐ +│ PostgreSQL Database (TypeORM) │ +│ ┌───────────────┐ ┌──────────────────┐ ┌──────────────┐│ +│ │UserSecretEntity│ │SharedSecretEntity│ │SecretAccess ││ +│ │ │ │ │ │LogEntity ││ +│ └───────────────┘ └──────────────────┘ └──────────────┘│ +└─────────────────────────────────────────────────────────────┘ +``` + +### Module Structure + +``` +backend/src/ +├── entities/ +│ ├── user-secret/ +│ │ ├── user-secret.entity.ts +│ │ ├── user-secret.interface.ts +│ │ └── use-cases/ +│ │ ├── create-user-secret.use.case.ts +│ │ ├── update-user-secret.use.case.ts +│ │ ├── delete-user-secret.use.case.ts +│ │ ├── find-user-secrets.use.case.ts +│ │ ├── share-secret.use.case.ts +│ │ └── revoke-secret-access.use.case.ts +│ ├── shared-secret/ +│ │ ├── shared-secret.entity.ts +│ │ └── shared-secret.interface.ts +│ └── secret-access-log/ +│ ├── secret-access-log.entity.ts +│ └── secret-access-log.interface.ts +├── modules/ +│ └── secrets/ +│ ├── secrets.module.ts +│ ├── secrets.controller.ts +│ ├── secrets.service.ts +│ ├── dto/ +│ │ ├── create-secret.dto.ts +│ │ ├── update-secret.dto.ts +│ │ ├── share-secret.dto.ts +│ │ └── find-secrets.dto.ts +│ └── guards/ +│ └── secret-access.guard.ts +└── migrations/ + ├── TIMESTAMP-CreateUserSecretEntity.ts + ├── TIMESTAMP-CreateSharedSecretEntity.ts + └── TIMESTAMP-CreateSecretAccessLogEntity.ts +``` + +### Data Flow + +#### Creating a Secret +``` +1. User submits form → Frontend validates +2. Frontend sends POST /secrets with encrypted master password (if enabled) +3. SecretsController receives request +4. JwtAuthGuard validates authentication +5. SecretsService.createSecret() called +6. Encryptor.encryptData(value) encrypts with PRIVATE_KEY +7. If master password: Encryptor.encryptDataMasterPwd() double-encrypts +8. UserSecretEntity created and saved +9. SecretAccessLogEntity created (action: CREATE) +10. Response sent to frontend (secret without value) +``` + +#### Reading a Secret +``` +1. User clicks secret → Frontend requests GET /secrets/:id +2. If master password required, frontend prompts and sends in header +3. SecretsController receives request +4. JwtAuthGuard validates authentication +5. SecretAccessGuard validates permissions +6. SecretsService.findSecretById() called +7. Entity loaded from database +8. If master password: validate hash, decrypt with master password +9. Decrypt with PRIVATE_KEY +10. SecretAccessLogEntity created (action: VIEW) +11. Response sent with decrypted value +``` + +#### Sharing a Secret +``` +1. Owner clicks "Share" → Selects recipient and permissions +2. Frontend sends POST /secrets/:id/share +3. SecretsController receives request +4. Validates owner permission +5. Validates recipient exists +6. SharedSecretEntity created +7. SecretAccessLogEntity created (action: SHARE) +8. Notification sent to recipient +9. Response confirms sharing +``` + +--- + +## Database Schema + +### UserSecretEntity + +```typescript +@Entity('user_secrets') +@Index(['companyId', 'slug'], { unique: true }) +export class UserSecretEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => CompanyInfoEntity) + @JoinColumn() + company: CompanyInfoEntity; + + @Column() + @Index() + companyId: string; + + @Column({ type: 'varchar', length: 255 }) + slug: string; + + @Column({ type: 'text' }) + encryptedValue: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + @Column({ type: 'timestamp', nullable: true }) + lastAccessedAt: Date; + + @Column({ type: 'timestamp', nullable: true }) + expiresAt: Date; + + @Column({ default: false }) + masterEncryption: boolean; + + @Column({ type: 'varchar', length: 255, nullable: true }) + masterHash: string; + + @OneToMany(() => SecretAccessLogEntity, (log) => log.secret) + accessLogs: SecretAccessLogEntity[]; + + @BeforeInsert() + @BeforeUpdate() + encryptCredentials() { + // Encrypt value with PRIVATE_KEY + if (this.encryptedValue && !this.masterEncryption) { + this.encryptedValue = Encryptor.encryptData(this.encryptedValue); + } + } + + @AfterLoad() + decryptCredentials() { + // Decrypt value with PRIVATE_KEY + if (this.encryptedValue && !this.masterEncryption) { + this.encryptedValue = Encryptor.decryptData(this.encryptedValue); + } + } +} +``` + +### SecretAccessLogEntity + +```typescript +@Entity('secret_access_logs') +export class SecretAccessLogEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => UserSecretEntity, (secret) => secret.accessLogs) + @JoinColumn() + secret: UserSecretEntity; + + @Column() + @Index() + secretId: string; + + @ManyToOne(() => UserEntity) + @JoinColumn() + user: UserEntity; + + @Column() + @Index() + userId: string; + + @Column({ + type: 'enum', + enum: SecretActionEnum, + }) + action: SecretActionEnum; + + @CreateDateColumn() + @Index() + accessedAt: Date; + + @Column({ type: 'varchar', length: 45, nullable: true }) + ipAddress: string; + + @Column({ type: 'text', nullable: true }) + userAgent: string; + + @Column({ default: true }) + success: boolean; + + @Column({ type: 'text', nullable: true }) + errorMessage: string; +} + +export enum SecretActionEnum { + CREATE = 'create', + VIEW = 'view', + COPY = 'copy', + UPDATE = 'update', + DELETE = 'delete', +} +``` + +### Database Indexes + +```sql +-- Performance indexes +CREATE INDEX idx_user_secrets_company_id ON user_secrets(company_id); +CREATE INDEX idx_user_secrets_created_at ON user_secrets(created_at); +CREATE INDEX idx_user_secrets_expires_at ON user_secrets(expires_at); +CREATE UNIQUE INDEX idx_user_secrets_company_slug ON user_secrets(company_id, slug); + +CREATE INDEX idx_secret_access_logs_secret_id ON secret_access_logs(secret_id); +CREATE INDEX idx_secret_access_logs_user_id ON secret_access_logs(user_id); +CREATE INDEX idx_secret_access_logs_accessed_at ON secret_access_logs(accessed_at); +``` + +--- + +## API Specification + +### Base Path +All endpoints under: `/api/secrets` + +### Authentication +All endpoints require JWT authentication via `Authorization: Bearer ` header or `rocketadmin_cookie` cookie. + +### Master Password Header +When master password is enabled for a secret: `masterpwd: ` + +### Endpoints + +#### 1. Create Secret +```http +POST /api/secrets +Content-Type: application/json +Authorization: Bearer +masterpwd: (optional) + +{ + "slug": "aws-api-key", + "value": "AKIAIOSFODNN7EXAMPLE", + "expiresAt": "2026-12-31T23:59:59Z", + "masterEncryption": true, + "masterPassword": "MyStrongPassword123!" +} + +Response: 201 Created +{ + "id": "uuid", + "slug": "aws-api-key", + "createdAt": "2025-11-21T10:00:00Z", + "updatedAt": "2025-11-21T10:00:00Z", + "expiresAt": "2026-12-31T23:59:59Z", + "masterEncryption": true, + "companyId": "company-uuid" +} + +Error: 409 Conflict (if slug already exists in company) +{ + "statusCode": 409, + "message": "Secret with this slug already exists in your company", + "error": "Conflict" +} +``` + +#### 2. Get All Secrets (List) +```http +GET /api/secrets?page=1&limit=20&search=aws +Authorization: Bearer + +Response: 200 OK +{ + "data": [ + { + "id": "uuid", + "slug": "aws-api-key", + "createdAt": "2025-11-21T10:00:00Z", + "updatedAt": "2025-11-21T10:00:00Z", + "lastAccessedAt": "2025-11-21T11:30:00Z", + "expiresAt": "2026-12-31T23:59:59Z", + "masterEncryption": true, + "createdBy": { + "id": "user-uuid", + "email": "user@example.com", + "name": "John Doe" + } + } + ], + "pagination": { + "total": 15, + "page": 1, + "limit": 20, + "totalPages": 1 + } +} +``` + +#### 3. Get Secret by Slug +```http +GET /api/secrets/:slug +Authorization: Bearer +masterpwd: (if master encryption enabled) + +Response: 200 OK +{ + "id": "uuid", + "slug": "aws-api-key", + "value": "AKIAIOSFODNN7EXAMPLE", + "createdAt": "2025-11-21T10:00:00Z", + "updatedAt": "2025-11-21T10:00:00Z", + "lastAccessedAt": "2025-11-21T11:30:00Z", + "expiresAt": "2026-12-31T23:59:59Z", + "masterEncryption": true, + "companyId": "company-uuid", + "createdBy": { + "id": "user-uuid", + "email": "user@example.com", + "name": "John Doe" + } +} + +Error: 403 Forbidden (if master password required but not provided or incorrect) +{ + "statusCode": 403, + "message": "Master password required", + "error": "Forbidden" +} +``` + +#### 4. Update Secret +```http +PUT /api/secrets/:slug +Content-Type: application/json +Authorization: Bearer +masterpwd: (if currently encrypted) + +{ + "value": "NEWAKIAIOSFODNN7EXAMPLE", + "expiresAt": "2027-12-31T23:59:59Z" +} + +Response: 200 OK +{ + "id": "uuid", + "slug": "aws-api-key", + "updatedAt": "2025-11-21T12:00:00Z", + "expiresAt": "2027-12-31T23:59:59Z" +} +``` + +#### 5. Delete Secret +```http +DELETE /api/secrets/:slug +Authorization: Bearer + +Response: 200 OK +{ + "message": "Secret deleted successfully", + "deletedAt": "2025-11-21T12:30:00Z" +} + +Error: 403 Forbidden (if not company member) +{ + "statusCode": 403, + "message": "You don't have permission to delete this secret", + "error": "Forbidden" +} +``` + +#### 6. Get Secret Audit Log +```http +GET /api/secrets/:slug/audit-log?page=1&limit=50 +Authorization: Bearer + +Response: 200 OK +{ + "data": [ + { + "id": "log-uuid", + "action": "view", + "user": { + "id": "user-uuid", + "email": "user@example.com", + "name": "John Doe" + }, + "accessedAt": "2025-11-21T11:30:00Z", + "ipAddress": "192.168.1.100", + "userAgent": "Mozilla/5.0...", + "success": true + } + ], + "pagination": { + "total": 25, + "page": 1, + "limit": 50, + "totalPages": 1 + } +} +``` + +### Error Responses + +```http +400 Bad Request - Validation error +{ + "statusCode": 400, + "message": ["slug should not be empty", "slug must match pattern ^[a-zA-Z0-9_-]+$", "value should not be empty"], + "error": "Bad Request" +} + +401 Unauthorized - Not authenticated +{ + "statusCode": 401, + "message": "Unauthorized", + "error": "Unauthorized" +} + +403 Forbidden - No permission +{ + "statusCode": 403, + "message": "You don't have permission to access this secret", + "error": "Forbidden" +} + +404 Not Found - Secret not found +{ + "statusCode": 404, + "message": "Secret not found", + "error": "Not Found" +} + +409 Conflict - Slug already exists +{ + "statusCode": 409, + "message": "Secret with this slug already exists in your company", + "error": "Conflict" +} + +410 Gone - Secret expired +{ + "statusCode": 410, + "message": "Secret has expired", + "error": "Gone" +} +``` + +--- + +## Security Design + +### Encryption Architecture + +#### Layer 1: Base Encryption (Always Active) +- **Algorithm**: AES-256 +- **Key**: `PRIVATE_KEY` environment variable (64+ characters) +- **Implementation**: `Encryptor.encryptData(value)` +- **Applied**: All secret values encrypted at rest +- **Lifecycle**: Encrypted on `@BeforeInsert/@BeforeUpdate`, decrypted on `@AfterLoad` + +#### Layer 2: Master Password Encryption (Optional) +- **Algorithm**: AES-256 +- **Key**: User-provided master password +- **Implementation**: `Encryptor.encryptDataMasterPwd(value, masterPwd)` +- **Applied**: When user enables `masterEncryption` flag +- **Validation**: Master password hash stored in `masterHash` field (PBKDF2) +- **Process**: Value encrypted with master password FIRST, then with PRIVATE_KEY + +#### Encryption Flow +``` +Plain Value → [Master Password Encrypt] → [PRIVATE_KEY Encrypt] → Stored in DB + +Retrieval: +Stored Value → [PRIVATE_KEY Decrypt] → [Master Password Decrypt] → Plain Value +``` + +### Access Control + +#### Permission Matrix + +| Action | Company Member | Non-member | +|--------|----------------|------------| +| View | ✓ | ✗ | +| Edit | ✓ | ✗ | +| Delete | ✓ | ✗ | +| Audit | ✓ | ✗ | + +#### SecretAccessGuard Implementation + +```typescript +@Injectable() +export class SecretAccessGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const user = request.user; // From JWT + const slug = request.params.slug; + + // Load secret by slug and company + const secret = await this.secretsRepository.findOne({ + where: { slug, companyId: user.companyId }, + relations: ['company'], + }); + + if (!secret) { + throw new NotFoundException('Secret not found'); + } + + // Check expiration + if (secret.expiresAt && secret.expiresAt < new Date()) { + throw new GoneException('Secret has expired'); + } + + // All company members have full access + return true; + } +} +``` + +### Audit Logging + +#### Log Creation Service + +```typescript +@Injectable() +export class SecretAuditService { + async logAccess( + secretId: string, + userId: string, + action: SecretActionEnum, + request: Request, + success: boolean = true, + errorMessage?: string, + ): Promise { + const log = new SecretAccessLogEntity(); + log.secretId = secretId; + log.userId = userId; + log.action = action; + log.accessedAt = new Date(); + log.ipAddress = this.getClientIp(request); + log.userAgent = request.headers['user-agent']; + log.success = success; + log.errorMessage = errorMessage; + + await this.secretAccessLogRepository.save(log); + + // Update lastAccessedAt on secret for VIEW actions + if (action === SecretActionEnum.VIEW && success) { + await this.secretRepository.update(secretId, { + lastAccessedAt: new Date(), + }); + } + } + + private getClientIp(request: Request): string { + return ( + (request.headers['x-forwarded-for'] as string)?.split(',')[0] || + (request.headers['x-real-ip'] as string) || + request.connection.remoteAddress || + request.socket.remoteAddress || + 'unknown' + ); + } +} +``` + +### Master Password Security + +#### Frontend Handling (CRITICAL CHANGE) +**Current Problem**: Master passwords stored in localStorage (vulnerable to XSS) + +**Solution**: +1. **Never store master passwords in localStorage** +2. **Session-only storage**: + ```typescript + // Store in memory only (component state) + private masterPasswords: Map = new Map(); + + // Or use sessionStorage with auto-clear + sessionStorage.setItem(`master_${secretId}`, password); + // Clear after 15 minutes + setTimeout(() => { + sessionStorage.removeItem(`master_${secretId}`); + }, 15 * 60 * 1000); + ``` + +3. **Re-prompt after timeout**: + - Store timestamp of last entry + - Require re-entry after 15 minutes + - Clear on browser close (sessionStorage) + +4. **Optional: "Remember for session" checkbox**: + - User must explicitly opt-in + - Still clears on browser close + - Never persist to localStorage + +#### Backend Validation + +```typescript +async validateMasterPassword( + secret: UserSecretEntity, + providedPassword: string, +): Promise { + if (!secret.masterEncryption) { + return true; + } + + if (!providedPassword) { + throw new ForbiddenException('Master password required'); + } + + const passwordValid = Encryptor.verifyUserPassword( + providedPassword, + secret.masterHash, + ); + + if (!passwordValid) { + // Log failed attempt + await this.auditService.logAccess( + secret.id, + 'current-user-id', + SecretActionEnum.VIEW, + request, + false, + 'Invalid master password', + ); + throw new ForbiddenException('Invalid master password'); + } + + return true; +} +``` + +### Rate Limiting + +Apply rate limits to prevent brute force attacks on master passwords: + +```typescript +@Controller('secrets') +@UseGuards(JwtAuthGuard) +export class SecretsController { + @Get(':id') + @Throttle(10, 60) // 10 requests per 60 seconds + async findOne( + @Param('id') id: string, + @Headers('masterpwd') masterPwd: string, + ) { + // ... + } +} +``` + +### Input Sanitization + +```typescript +export class CreateSecretDto { + @IsString() + @IsNotEmpty() + @MinLength(1) + @MaxLength(255) + @Matches(/^[a-zA-Z0-9_-]+$/, { + message: 'slug must contain only letters, numbers, hyphens, and underscores' + }) + @Transform(({ value }) => value.trim()) + slug: string; + + @IsString() + @IsNotEmpty() + @MinLength(1) + @MaxLength(10000) + value: string; // Don't trim (may be intentional whitespace) + + @IsOptional() + @IsISO8601() + @IsDateInFuture() + expiresAt?: string; + + @IsBoolean() + @IsOptional() + masterEncryption?: boolean; + + @IsString() + @IsOptional() + @MinLength(8) + @ValidateIf((o) => o.masterEncryption === true) + masterPassword?: string; +} +``` + +### SQL Injection Prevention +- **TypeORM** handles parameterization automatically +- Never use raw queries with user input +- Use QueryBuilder for complex queries + +### XSS Prevention +- Frontend sanitizes all output using Angular's built-in DomSanitizer +- Content-Security-Policy headers set +- Secret values displayed in `
` tags or ``
+
+---
+
+## Frontend Requirements
+
+### New Components
+
+#### 1. Secrets List Component
+**Path**: `frontend/src/app/components/secrets/secrets-list/secrets-list.component.ts`
+
+**Features**:
+- Displays company secrets in table/card view
+- Columns: Slug, Last Accessed, Expires, Actions
+- Search bar (filters by slug only)
+- Pagination
+- Action buttons: View, Edit, Delete (all available to company members)
+
+#### 2. Secret Details Component
+**Path**: `frontend/src/app/components/secrets/secret-details/secret-details.component.ts`
+
+**Features**:
+- Displays secret metadata
+- Secret value initially masked (****)
+- "Reveal" button to show value
+- "Copy to Clipboard" button with auto-clear after 30 seconds
+- Master password prompt dialog (if required)
+- Edit mode (available to all company members)
+- Audit log tab showing access history (available to all company members)
+
+#### 3. Create/Edit Secret Dialog
+**Path**: `frontend/src/app/components/secrets/secret-form-dialog/secret-form-dialog.component.ts`
+
+**Features**:
+- Form fields: Slug (disabled on edit), Value
+- Slug validation (letters, numbers, hyphens, underscores only)
+- Master password toggle
+- Master password input (confirmation required)
+- Expiration date picker
+- Validation error display
+
+#### 4. Master Password Prompt Dialog
+**Path**: `frontend/src/app/components/secrets/master-password-prompt/master-password-prompt.component.ts`
+
+**Features**:
+- Password input field
+- "Remember for session" checkbox (stores in sessionStorage)
+- Cancel/Submit buttons
+- Error message display
+
+#### 5. Audit Log Component
+**Path**: `frontend/src/app/components/secrets/audit-log/audit-log.component.ts`
+
+**Features**:
+- Table with columns: User, Action, Timestamp, IP Address, Status
+- Pagination
+- Filter by action type
+- Date range picker
+
+### Navigation Updates
+
+Add "Secrets" menu item to main navigation:
+```typescript
+// frontend/src/app/app-routing.module.ts
+{
+  path: 'secrets',
+  loadChildren: () => import('./secrets/secrets.module').then(m => m.SecretsModule),
+  canActivate: [AuthGuard],
+}
+```
+
+### Service Layer
+
+#### SecretsService
+**Path**: `frontend/src/app/services/secrets.service.ts`
+
+```typescript
+@Injectable({ providedIn: 'root' })
+export class SecretsService {
+  private apiUrl = '/api/secrets';
+
+  constructor(private http: HttpClient) {}
+
+  getSecrets(params?: SecretListParams): Observable {
+    return this.http.get(this.apiUrl, { params });
+  }
+
+  getSecretById(id: string, masterPassword?: string): Observable {
+    const headers = masterPassword ? { masterpwd: masterPassword } : {};
+    return this.http.get(`${this.apiUrl}/${id}`, { headers });
+  }
+
+  createSecret(secret: CreateSecretDto): Observable {
+    return this.http.post(this.apiUrl, secret);
+  }
+
+  updateSecret(id: string, secret: UpdateSecretDto, masterPassword?: string): Observable {
+    const headers = masterPassword ? { masterpwd: masterPassword } : {};
+    return this.http.put(`${this.apiUrl}/${id}`, secret, { headers });
+  }
+
+  deleteSecret(id: string): Observable {
+    return this.http.delete(`${this.apiUrl}/${id}`);
+  }
+
+  getAuditLog(id: string, page: number = 1): Observable {
+    return this.http.get(`${this.apiUrl}/${id}/audit-log`, {
+      params: { page: page.toString(), limit: '50' },
+    });
+  }
+}
+```
+
+#### MasterPasswordManager
+**Path**: `frontend/src/app/services/master-password-manager.service.ts`
+
+```typescript
+@Injectable({ providedIn: 'root' })
+export class MasterPasswordManager {
+  private passwords = new Map();
+  private readonly TIMEOUT_MINUTES = 15;
+
+  storePassword(secretId: string, password: string, rememberForSession: boolean): void {
+    if (!rememberForSession) {
+      // Store in memory only
+      this.passwords.set(secretId, {
+        password,
+        expiresAt: new Date(Date.now() + this.TIMEOUT_MINUTES * 60 * 1000),
+      });
+      return;
+    }
+
+    // Store in sessionStorage (cleared on browser close)
+    const data = {
+      password,
+      expiresAt: Date.now() + this.TIMEOUT_MINUTES * 60 * 1000,
+    };
+    sessionStorage.setItem(`master_${secretId}`, JSON.stringify(data));
+
+    // Also store in memory for quick access
+    this.passwords.set(secretId, {
+      password,
+      expiresAt: new Date(data.expiresAt),
+    });
+  }
+
+  getPassword(secretId: string): string | null {
+    // Check memory first
+    const memoryEntry = this.passwords.get(secretId);
+    if (memoryEntry && memoryEntry.expiresAt > new Date()) {
+      return memoryEntry.password;
+    }
+
+    // Check sessionStorage
+    const stored = sessionStorage.getItem(`master_${secretId}`);
+    if (stored) {
+      try {
+        const data = JSON.parse(stored);
+        if (data.expiresAt > Date.now()) {
+          return data.password;
+        }
+        // Expired, remove
+        sessionStorage.removeItem(`master_${secretId}`);
+      } catch (e) {
+        sessionStorage.removeItem(`master_${secretId}`);
+      }
+    }
+
+    // Expired or not found
+    this.passwords.delete(secretId);
+    return null;
+  }
+
+  clearPassword(secretId: string): void {
+    this.passwords.delete(secretId);
+    sessionStorage.removeItem(`master_${secretId}`);
+  }
+
+  clearAll(): void {
+    this.passwords.clear();
+    // Clear all master passwords from sessionStorage
+    Object.keys(sessionStorage)
+      .filter(key => key.startsWith('master_'))
+      .forEach(key => sessionStorage.removeItem(key));
+  }
+}
+```
+
+### UI/UX Considerations
+
+1. **Copy to Clipboard**:
+   - Show success toast
+   - Auto-clear clipboard after 30 seconds (optional)
+   - Use Clipboard API with fallback
+
+2. **Secret Value Display**:
+   - Initially show as `••••••••••••`
+   - "Reveal" button changes to "Hide"
+   - When revealed, show in monospace font
+   - Use `` for easy reveal/hide toggle
+
+3. **Master Password UX**:
+   - Prompt appears as modal dialog
+   - Show "forgot password" hint: "Contact secret owner"
+   - After 3 failed attempts, show captcha or rate limit
+
+4. **Expiration Warnings**:
+   - Show badge if expires within 7 days
+   - Show different badge if expired
+   - Toast notification when secret is about to expire
+
+5. **Responsive Design**:
+   - Mobile-friendly table (convert to cards)
+   - Touch-friendly buttons
+   - Full-screen dialogs on mobile
+
+6. **Accessibility**:
+   - ARIA labels for all interactive elements
+   - Keyboard navigation support
+   - Screen reader announcements
+   - High contrast mode support
+
+---
+
+## Implementation Phases
+
+### Phase 1: Backend Foundation (Week 1-2)
+**Goal**: Core backend functionality
+
+**Tasks**:
+1. Create TypeORM entities:
+   - UserSecretEntity
+   - SecretAccessLogEntity
+2. Create database migrations
+3. Implement SecretsService with CRUD methods
+4. Implement encryption/decryption using existing Encryptor
+5. Implement SecretAccessGuard
+6. Create SecretsController with endpoints:
+   - POST /secrets
+   - GET /secrets
+   - GET /secrets/:id
+   - PUT /secrets/:id
+   - DELETE /secrets/:id
+   - GET /secrets/:id/audit-log
+7. Add audit logging to all operations
+8. Write unit tests for service layer
+9. Write e2e tests for API endpoints
+
+**Deliverables**:
+- Working API for secret CRUD
+- Encrypted storage
+- Company-based access control
+- Audit logging
+- Test coverage >80%
+
+### Phase 2: Frontend Implementation (Week 3-4)
+**Goal**: Complete user interface
+
+**Tasks**:
+1. Create Angular module and routing
+2. Implement SecretsService (HTTP client)
+3. Implement MasterPasswordManager
+4. Create components:
+   - secrets-list
+   - secret-details
+   - secret-form-dialog
+   - master-password-prompt
+   - audit-log
+5. Add navigation menu item
+6. Implement responsive design
+7. Add loading states and error handling
+8. Write frontend unit tests
+
+**Deliverables**:
+- Complete UI for all features
+- Responsive design
+- Error handling
+- Frontend tests
+
+### Phase 3: Polish & Testing (Week 5)
+**Goal**: Final touches and production readiness
+
+**Tasks**:
+1. Implement secret expiration handling
+2. Add expiration notifications
+3. UI polish and refinements
+4. Performance optimization
+5. Security audit
+6. Documentation
+7. Integration testing
+8. Load testing
+
+**Deliverables**:
+- Expiration system complete
+- Documentation complete
+- Ready for production
+
+### Phase 4: Launch (Week 6)
+**Goal**: Production deployment
+
+**Tasks**:
+1. Security penetration testing
+2. User acceptance testing
+3. Deploy to staging
+4. Monitor and fix issues
+5. Deploy to production
+6. Post-launch monitoring
+
+**Deliverables**:
+- Stable production deployment
+- Zero critical bugs
+- Monitoring dashboards
+
+---
+
+## Testing Strategy
+
+### Unit Tests
+
+#### Backend
+```typescript
+// secrets.service.spec.ts
+describe('SecretsService', () => {
+  it('should create a secret with encryption', async () => {
+    const dto = { title: 'Test', value: 'secret', secretType: 'api_key' };
+    const result = await service.createSecret(dto, user);
+    expect(result.encryptedValue).not.toBe('secret');
+    expect(result.title).toBe('Test');
+  });
+
+  it('should decrypt secret value when loading', async () => {
+    const secret = await service.findById(secretId, user);
+    expect(secret.value).toBe('original_value');
+  });
+
+  it('should require master password when enabled', async () => {
+    await expect(
+      service.findById(secretId, user, null)
+    ).rejects.toThrow('Master password required');
+  });
+
+  it('should log all access attempts', async () => {
+    await service.findById(secretId, user);
+    const logs = await auditService.getLogs(secretId);
+    expect(logs.length).toBe(1);
+    expect(logs[0].action).toBe('view');
+  });
+});
+
+// secret-access.guard.spec.ts
+describe('SecretAccessGuard', () => {
+  it('should allow owner full access', async () => {
+    const canActivate = await guard.canActivate(context);
+    expect(canActivate).toBe(true);
+  });
+
+  it('should allow shared read access', async () => {
+    const canActivate = await guard.canActivate(context);
+    expect(canActivate).toBe(true);
+  });
+
+  it('should deny shared write access for read-only share', async () => {
+    await expect(guard.canActivate(context)).rejects.toThrow('read-only access');
+  });
+
+  it('should deny access to non-members', async () => {
+    await expect(guard.canActivate(context)).rejects.toThrow('permission');
+  });
+});
+```
+
+#### Frontend
+```typescript
+// secrets.service.spec.ts
+describe('SecretsService', () => {
+  it('should fetch secrets list', (done) => {
+    service.getSecrets().subscribe(response => {
+      expect(response.data.length).toBeGreaterThan(0);
+      done();
+    });
+  });
+
+  it('should send master password in header', (done) => {
+    service.getSecretById('id', 'password').subscribe(() => {
+      const req = httpMock.expectOne('/api/secrets/id');
+      expect(req.request.headers.get('masterpwd')).toBe('password');
+      done();
+    });
+  });
+});
+
+// master-password-manager.spec.ts
+describe('MasterPasswordManager', () => {
+  it('should store password in memory', () => {
+    manager.storePassword('secret-id', 'password', false);
+    expect(manager.getPassword('secret-id')).toBe('password');
+  });
+
+  it('should expire password after timeout', fakeAsync(() => {
+    manager.storePassword('secret-id', 'password', false);
+    tick(16 * 60 * 1000); // 16 minutes
+    expect(manager.getPassword('secret-id')).toBeNull();
+  }));
+
+  it('should clear all passwords', () => {
+    manager.storePassword('secret-1', 'password', false);
+    manager.storePassword('secret-2', 'password', false);
+    manager.clearAll();
+    expect(manager.getPassword('secret-1')).toBeNull();
+    expect(manager.getPassword('secret-2')).toBeNull();
+  });
+});
+```
+
+### Integration Tests
+
+```typescript
+// secrets.e2e.spec.ts
+describe('Secrets API (e2e)', () => {
+  it('should create, read, update, and delete secret', async () => {
+    // Create
+    const createResponse = await request(app.getHttpServer())
+      .post('/api/secrets')
+      .set('Authorization', `Bearer ${jwtToken}`)
+      .send({ title: 'Test Secret', value: 'secret123', secretType: 'api_key' })
+      .expect(201);
+
+    const secretId = createResponse.body.id;
+
+    // Read
+    const readResponse = await request(app.getHttpServer())
+      .get(`/api/secrets/${secretId}`)
+      .set('Authorization', `Bearer ${jwtToken}`)
+      .expect(200);
+
+    expect(readResponse.body.value).toBe('secret123');
+
+    // Update
+    await request(app.getHttpServer())
+      .put(`/api/secrets/${secretId}`)
+      .set('Authorization', `Bearer ${jwtToken}`)
+      .send({ title: 'Updated Secret', value: 'newsecret', secretType: 'api_key' })
+      .expect(200);
+
+    // Delete
+    await request(app.getHttpServer())
+      .delete(`/api/secrets/${secretId}`)
+      .set('Authorization', `Bearer ${jwtToken}`)
+      .expect(200);
+  });
+
+  it('should enforce master password protection', async () => {
+    const secretId = await createSecretWithMasterPassword('password123');
+
+    // Should fail without password
+    await request(app.getHttpServer())
+      .get(`/api/secrets/${secretId}`)
+      .set('Authorization', `Bearer ${jwtToken}`)
+      .expect(403);
+
+    // Should succeed with correct password
+    await request(app.getHttpServer())
+      .get(`/api/secrets/${secretId}`)
+      .set('Authorization', `Bearer ${jwtToken}`)
+      .set('masterpwd', 'password123')
+      .expect(200);
+  });
+
+  it('should allow company members to view secrets', async () => {
+    const secret = await createSecret(user1);
+
+    // User2 from same company should be able to read
+    await request(app.getHttpServer())
+      .get(`/api/secrets/${secret.id}`)
+      .set('Authorization', `Bearer ${user2Token}`)
+      .expect(200);
+
+    // User2 should NOT be able to edit
+    await request(app.getHttpServer())
+      .put(`/api/secrets/${secret.id}`)
+      .set('Authorization', `Bearer ${user2Token}`)
+      .send({ title: 'Updated', value: 'newvalue' })
+      .expect(403);
+
+    // User from different company should NOT have access
+    await request(app.getHttpServer())
+      .get(`/api/secrets/${secret.id}`)
+      .set('Authorization', `Bearer ${user3DifferentCompanyToken}`)
+      .expect(403);
+  });
+});
+```
+
+### Security Tests
+
+```typescript
+// security.e2e.spec.ts
+describe('Security Tests', () => {
+  it('should prevent SQL injection in search', async () => {
+    const maliciousQuery = "'; DROP TABLE user_secrets; --";
+    await request(app.getHttpServer())
+      .get('/api/secrets')
+      .query({ search: maliciousQuery })
+      .set('Authorization', `Bearer ${jwtToken}`)
+      .expect(200); // Should not cause error
+
+    // Verify table still exists
+    const count = await secretRepository.count();
+    expect(count).toBeGreaterThan(0);
+  });
+
+  it('should prevent XSS in secret values', async () => {
+    const xssPayload = '';
+    const secret = await service.createSecret({
+      title: xssPayload,
+      value: xssPayload,
+      secretType: 'other',
+    }, user);
+
+    const retrieved = await service.findById(secret.id, user);
+    expect(retrieved.title).toBe(xssPayload); // Stored as-is
+    // Frontend should sanitize when displaying
+  });
+
+  it('should rate limit master password attempts', async () => {
+    const secretId = await createSecretWithMasterPassword('correct');
+
+    // Try 20 times with wrong password
+    const requests = [];
+    for (let i = 0; i < 20; i++) {
+      requests.push(
+        request(app.getHttpServer())
+          .get(`/api/secrets/${secretId}`)
+          .set('Authorization', `Bearer ${jwtToken}`)
+          .set('masterpwd', 'wrong')
+      );
+    }
+
+    const responses = await Promise.all(requests);
+    const tooManyRequests = responses.filter(r => r.status === 429);
+    expect(tooManyRequests.length).toBeGreaterThan(0);
+  });
+
+  it('should not leak secret existence in 404 vs 403', async () => {
+    const nonExistentId = '00000000-0000-0000-0000-000000000000';
+
+    // Non-existent secret
+    const res1 = await request(app.getHttpServer())
+      .get(`/api/secrets/${nonExistentId}`)
+      .set('Authorization', `Bearer ${jwtToken}`)
+      .expect(404);
+
+    // Existing secret, no permission
+    const secretId = await createSecretForDifferentUser();
+    const res2 = await request(app.getHttpServer())
+      .get(`/api/secrets/${secretId}`)
+      .set('Authorization', `Bearer ${jwtToken}`)
+      .expect(404); // Should also be 404, not 403
+
+    // Both should have same message
+    expect(res1.body.message).toBe(res2.body.message);
+  });
+});
+```
+
+### Performance Tests
+
+```typescript
+// performance.spec.ts
+describe('Performance Tests', () => {
+  it('should handle 1000 secrets efficiently', async () => {
+    // Create 1000 secrets
+    await Promise.all(
+      Array.from({ length: 1000 }, (_, i) =>
+        service.createSecret({
+          title: `Secret ${i}`,
+          value: `value${i}`,
+          secretType: 'api_key',
+        }, user)
+      )
+    );
+
+    // List should be fast
+    const start = Date.now();
+    await service.findAll({ userId: user.id, page: 1, limit: 20 });
+    const duration = Date.now() - start;
+    expect(duration).toBeLessThan(100); // <100ms
+  });
+
+  it('should decrypt secrets in parallel', async () => {
+    const secretIds = await createMultipleSecrets(100);
+
+    const start = Date.now();
+    await Promise.all(secretIds.map(id => service.findById(id, user)));
+    const duration = Date.now() - start;
+
+    expect(duration).toBeLessThan(1000); // <1s for 100 secrets
+  });
+});
+```
+
+---
+
+## Migration and Rollout
+
+### Database Migration Strategy
+
+#### Step 1: Deploy Migration (Backward Compatible)
+```bash
+# Run migrations
+npm run migration:run
+
+# Verify tables created
+npm run migration:show
+```
+
+#### Step 2: Deploy Backend (Feature Flag)
+```typescript
+// Add feature flag to environment
+SECRETS_FEATURE_ENABLED=true
+
+// In secrets.controller.ts
+@Controller('secrets')
+@UseGuards(FeatureFlagGuard('SECRETS_FEATURE_ENABLED'))
+export class SecretsController {
+  // ...
+}
+```
+
+#### Step 3: Deploy Frontend (Progressive Rollout)
+```typescript
+// Use feature flag service
+if (this.featureFlags.isEnabled('secrets')) {
+  // Show secrets menu item
+}
+```
+
+#### Step 4: Enable for Beta Users
+- Enable for internal team first
+- Monitor for errors and performance issues
+- Gather feedback
+
+#### Step 5: Full Rollout
+- Enable for 10% of users
+- Monitor metrics (error rates, usage, performance)
+- Gradually increase to 100%
+
+### Rollback Plan
+
+If critical issues occur:
+
+1. **Disable Feature Flag**:
+   ```bash
+   # Backend
+   SECRETS_FEATURE_ENABLED=false
+
+   # Frontend
+   Feature flag service will hide UI
+   ```
+
+2. **Data Remains Safe**:
+   - Database tables remain
+   - Data is encrypted and safe
+   - No data loss
+
+3. **Fix and Redeploy**:
+   - Fix issues in staging
+   - Re-enable feature flag
+
+### Migration from Beta (If Applicable)
+
+If secrets were stored elsewhere (e.g., connection descriptions):
+
+```typescript
+// Migration script: migrate-legacy-secrets.ts
+async function migrateLegacySecrets() {
+  // Find connections with secrets in description
+  const connections = await connectionRepository.find({
+    where: { description: Like('%API_KEY:%') },
+  });
+
+  for (const connection of connections) {
+    // Parse secrets from description
+    const secrets = parseSecretsFromDescription(connection.description);
+
+    // Create new secret entities
+    for (const secret of secrets) {
+      await secretRepository.save({
+        userId: connection.authorId,
+        title: `${connection.title} - ${secret.name}`,
+        value: secret.value,
+        secretType: 'api_key',
+        tags: ['migrated', connection.title],
+      });
+    }
+
+    // Remove from description
+    connection.description = removeSecretsFromDescription(connection.description);
+    await connectionRepository.save(connection);
+  }
+}
+```
+
+---
+
+## Future Enhancements
+
+### Phase 2 Features (Post-Launch)
+
+#### 1. User-to-User Secret Sharing
+- Share secrets with specific users
+- Permission levels (read-only, read-write)
+- Share expiration dates
+- Revoke access
+- View who has access to each secret
+
+#### 2. Tags and Categorization
+- Add multiple tags to secrets
+- Filter by tags
+- Organize secrets by category
+- Tag-based search
+
+#### 3. Secret Types
+- Predefined secret types (API Key, Token, Password, Certificate, SSH Key, etc.)
+- Type-specific validation
+- Filter by secret type
+- Type-based templates
+
+#### 4. Descriptions
+- Add detailed descriptions to secrets
+- Search by description
+- Rich text support
+
+#### 5. External Vault Integration
+- **AWS Secrets Manager**: Store/retrieve secrets from AWS
+- **HashiCorp Vault**: Enterprise secret management
+- **Azure Key Vault**: Azure secret storage
+- Configuration per company or per secret
+
+#### 6. Secret Versioning
+- Keep history of secret changes
+- Restore previous versions
+- Diff view between versions
+
+#### 7. Secret Rotation
+- Automatic expiration warnings
+- Integration with external APIs for rotation
+- Rotation policies
+
+#### 8. Secret Templates
+- Predefined templates for common services (AWS, GitHub, Stripe)
+- Auto-populate fields
+- Validation for each template
+
+#### 9. Secret Generators
+- Password generator with strength indicator
+- API key generator
+- SSH key pair generator
+
+#### 10. Import/Export
+- Encrypted export format
+- Import from 1Password, LastPass, etc.
+- Bulk operations
+
+#### 11. Browser Extension
+- Auto-fill secrets in browser
+- Capture new secrets automatically
+- Secure communication with main app
+
+#### 12. CLI Tool
+- Manage secrets via command line
+- Integration with CI/CD pipelines
+- Environment variable injection
+
+#### 13. Mobile App
+- iOS/Android apps
+- Biometric authentication
+- Offline access (encrypted)
+
+#### 14. Advanced Audit
+- Anomaly detection (unusual access patterns)
+- Real-time alerts
+- Compliance reports (SOC 2, HIPAA)
+
+#### 15. Secret Dependencies
+- Link secrets to connections/resources
+- Cascade delete warnings
+- Dependency graph
+
+---
+
+## Appendix
+
+### Glossary
+
+- **Secret**: Sensitive information (API key, password, token, etc.) stored encrypted
+- **Master Password**: Optional additional password layer for encrypting specific secrets
+- **PRIVATE_KEY**: Application-wide encryption key from environment variable
+- **Share**: Granting access to a secret to another user
+- **Owner**: User who created the secret
+- **Audit Log**: Record of all access and modifications to a secret
+- **Company Secret**: Secret accessible to all users in a company
+
+### References
+
+- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
+- [NIST Password Guidelines](https://pages.nist.gov/800-63-3/)
+- [TypeORM Encryption](https://typeorm.io/)
+- [NestJS Security Best Practices](https://docs.nestjs.com/security/encryption-and-hashing)
+- [Angular Security Guide](https://angular.io/guide/security)
+
+### Environment Variables Required
+
+```bash
+# Existing (required for encryption)
+PRIVATE_KEY=your-64-character-or-longer-encryption-key
+
+# New (optional)
+SECRETS_FEATURE_ENABLED=true  # Feature flag
+SECRET_EXPIRATION_CHECK_CRON="0 0 * * *"  # Daily at midnight
+```
+
+### Configuration Examples
+
+#### TypeORM Entity Registration
+```typescript
+// backend/src/app.module.ts
+TypeOrmModule.forRoot({
+  // ...
+  entities: [
+    // Existing entities...
+    UserSecretEntity,
+    SharedSecretEntity,
+    SecretAccessLogEntity,
+  ],
+}),
+```
+
+#### Angular Lazy Loading
+```typescript
+// frontend/src/app/app-routing.module.ts
+{
+  path: 'secrets',
+  loadChildren: () =>
+    import('./components/secrets/secrets.module').then(m => m.SecretsModule),
+  canActivate: [AuthGuard],
+  data: { title: 'Secrets' },
+}
+```
+
+---
+
+**End of Specification**
+
+**Next Steps**:
+1. Review and approve specification
+2. Set up project board with tasks
+3. Assign developers to phases
+4. Begin Phase 1 implementation
+
+**Questions or Clarifications**:
+- Contact: [Your Team]
+- Slack: #secrets-feature
+- Epic: JIRA-XXX
diff --git a/backend/Dockerfile b/backend/Dockerfile
deleted file mode 100644
index 94d9a4b59..000000000
--- a/backend/Dockerfile
+++ /dev/null
@@ -1,33 +0,0 @@
-FROM public.ecr.aws/docker/library/node:18
-RUN apt-get update && apt-get install -y \
-    tini \
- && rm -rf /var/lib/apt/lists/*
-WORKDIR /app
-COPY package.json yarn.lock nest-cli.json /app/
-RUN yarn install --network-timeout 1000000
-COPY . /app
-RUN yarn run nest build
-CMD [ "tini", "node", "--", "--enable-source-maps", "dist/main.js"]
-ENTRYPOINT ["/app/entrypoint.sh"]
-
-RUN apt update && \
-    apt install libaio1 libc6 curl -y && \
-    cd /tmp && \
-    curl -o instantclient-basiclite.zip https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linuxx64.zip -sl && \
-    unzip instantclient-basiclite.zip && \
-    mv instantclient*/ /usr/lib/instantclient && \
-    rm instantclient-basiclite.zip && \
-    ln -s /usr/lib/instantclient/libclntsh.so.19.1 /usr/lib/libclntsh.so && \
-    ln -s /usr/lib/instantclient/libocci.so.19.1 /usr/lib/libocci.so && \
-    ln -s /usr/lib/instantclient/libociicus.so /usr/lib/libociicus.so && \
-    ln -s /usr/lib/instantclient/libnnz19.so /usr/lib/libnnz19.so && \
-    ln -s /usr/lib/libnsl.so.2 /usr/lib/libnsl.so.1 && \
-    ln -s /lib/libc.so.6 /usr/lib/libresolv.so.2 && \
-    ln -s /lib64/ld-linux-x86-64.so.2 /usr/lib/ld-linux-x86-64.so.2
-
-ENV ORACLE_BASE /usr/lib/instantclient
-ENV LD_LIBRARY_PATH /usr/lib/instantclient
-ENV TNS_ADMIN /usr/lib/instantclient
-ENV ORACLE_HOME /usr/lib/instantclient
-ENV TYPEORM_CONNECTION postgres
-ENV TYPEORM_MIGRATIONS dist/src/migrations/*.js
diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts
index 6660a4b40..1a875c722 100644
--- a/backend/src/app.module.ts
+++ b/backend/src/app.module.ts
@@ -39,6 +39,7 @@ import { GetHelloUseCase } from './use-cases-app/get-hello.use.case.js';
 import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
 import { SharedJobsModule } from './entities/shared-jobs/shared-jobs.module.js';
 import { TableCategoriesModule } from './entities/table-categories/table-categories.module.js';
+import { UserSecretModule } from './entities/user-secret/user-secret.module.js';
 
 @Module({
   imports: [
@@ -81,6 +82,7 @@ import { TableCategoriesModule } from './entities/table-categories/table-categor
     LoggingModule,
     SharedJobsModule,
     TableCategoriesModule,
+    UserSecretModule,
   ],
   controllers: [AppController],
   providers: [
diff --git a/backend/src/decorators/company-id.decorator.ts b/backend/src/decorators/company-id.decorator.ts
new file mode 100644
index 000000000..8f0b01e0c
--- /dev/null
+++ b/backend/src/decorators/company-id.decorator.ts
@@ -0,0 +1,23 @@
+import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common';
+import { IRequestWithCognitoInfo } from '../authorization/index.js';
+import { Messages } from '../exceptions/text/messages.js';
+
+export const CompanyId = createParamDecorator(async (data: any, ctx: ExecutionContext): Promise => {
+  const request: IRequestWithCognitoInfo = ctx.switchToHttp().getRequest();
+  const userId = request.decoded?.sub;
+
+  if (!userId) {
+    throw new BadRequestException(Messages.USER_ID_MISSING);
+  }
+
+  // Company ID should be retrieved from the user entity via a repository lookup
+  // This is a simplified version - in practice, you'd inject a UserRepository
+  // For now, we'll assume the company ID is added to the request by middleware
+  const companyId = (request as any).companyId;
+
+  if (!companyId) {
+    throw new BadRequestException('Company ID not found for user');
+  }
+
+  return companyId;
+});
diff --git a/backend/src/entities/secret-access-log/secret-access-log.entity.ts b/backend/src/entities/secret-access-log/secret-access-log.entity.ts
new file mode 100644
index 000000000..6ccc928a8
--- /dev/null
+++ b/backend/src/entities/secret-access-log/secret-access-log.entity.ts
@@ -0,0 +1,55 @@
+import { Column, CreateDateColumn, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn, Relation } from 'typeorm';
+import { UserEntity } from '../user/user.entity.js';
+import { UserSecretEntity } from '../user-secret/user-secret.entity.js';
+
+export enum SecretActionEnum {
+  CREATE = 'create',
+  VIEW = 'view',
+  COPY = 'copy',
+  UPDATE = 'update',
+  DELETE = 'delete',
+}
+
+@Entity('secret_access_logs')
+export class SecretAccessLogEntity {
+  @PrimaryGeneratedColumn('uuid')
+  id: string;
+
+  @ManyToOne(() => UserSecretEntity, (secret) => secret.accessLogs, { onDelete: 'CASCADE' })
+  @JoinColumn()
+  secret: Relation;
+
+  @Column()
+  @Index()
+  secretId: string;
+
+  @ManyToOne(() => UserEntity, { onDelete: 'CASCADE' })
+  @JoinColumn()
+  user: Relation;
+
+  @Column()
+  @Index()
+  userId: string;
+
+  @Column({
+    type: 'enum',
+    enum: SecretActionEnum,
+  })
+  action: SecretActionEnum;
+
+  @CreateDateColumn()
+  @Index()
+  accessedAt: Date;
+
+  @Column({ type: 'varchar', length: 45, nullable: true })
+  ipAddress: string;
+
+  @Column({ type: 'text', nullable: true })
+  userAgent: string;
+
+  @Column({ default: true })
+  success: boolean;
+
+  @Column({ type: 'text', nullable: true })
+  errorMessage: string;
+}
diff --git a/backend/src/entities/user-secret/application/dto/audit-log.dto.ts b/backend/src/entities/user-secret/application/dto/audit-log.dto.ts
new file mode 100644
index 000000000..a22f2c215
--- /dev/null
+++ b/backend/src/entities/user-secret/application/dto/audit-log.dto.ts
@@ -0,0 +1,44 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { SecretActionEnum } from '../../../secret-access-log/secret-access-log.entity.js';
+
+export class AuditLogEntryDto {
+  @ApiProperty()
+  id: string;
+
+  @ApiProperty({ enum: SecretActionEnum })
+  action: SecretActionEnum;
+
+  @ApiProperty()
+  user: {
+    id: string;
+    email: string;
+  };
+
+  @ApiProperty()
+  accessedAt: Date;
+
+  @ApiProperty({ required: false })
+  ipAddress?: string;
+
+  @ApiProperty({ required: false })
+  userAgent?: string;
+
+  @ApiProperty()
+  success: boolean;
+
+  @ApiProperty({ required: false })
+  errorMessage?: string;
+}
+
+export class AuditLogResponseDto {
+  @ApiProperty({ type: [AuditLogEntryDto] })
+  data: AuditLogEntryDto[];
+
+  @ApiProperty()
+  pagination: {
+    total: number;
+    page: number;
+    limit: number;
+    totalPages: number;
+  };
+}
diff --git a/backend/src/entities/user-secret/application/dto/create-secret.dto.ts b/backend/src/entities/user-secret/application/dto/create-secret.dto.ts
new file mode 100644
index 000000000..0f9546293
--- /dev/null
+++ b/backend/src/entities/user-secret/application/dto/create-secret.dto.ts
@@ -0,0 +1,40 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { Transform } from 'class-transformer';
+import { IsBoolean, IsISO8601, IsNotEmpty, IsOptional, IsString, Matches, MaxLength, MinLength, ValidateIf } from 'class-validator';
+
+export class CreateSecretDto {
+  @ApiProperty({ required: true, type: 'string' })
+  @IsString()
+  @IsNotEmpty()
+  @MinLength(1)
+  @MaxLength(255)
+  @Matches(/^[a-zA-Z0-9_-]+$/, {
+    message: 'slug must contain only letters, numbers, hyphens, and underscores',
+  })
+  @Transform(({ value }) => value.trim())
+  slug: string;
+
+  @ApiProperty({ required: true, type: 'string' })
+  @IsString()
+  @IsNotEmpty()
+  @MinLength(1)
+  @MaxLength(10000)
+  value: string;
+
+  @ApiProperty({ required: false, type: 'string' })
+  @IsOptional()
+  @IsISO8601()
+  expiresAt?: string;
+
+  @ApiProperty({ required: false, type: 'boolean' })
+  @IsBoolean()
+  @IsOptional()
+  masterEncryption?: boolean;
+
+  @ApiProperty({ required: false, type: 'string' })
+  @IsString()
+  @IsOptional()
+  @MinLength(8)
+  @ValidateIf((o) => o.masterEncryption === true)
+  masterPassword?: string;
+}
diff --git a/backend/src/entities/user-secret/application/dto/found-secret.dto.ts b/backend/src/entities/user-secret/application/dto/found-secret.dto.ts
new file mode 100644
index 000000000..9b926610b
--- /dev/null
+++ b/backend/src/entities/user-secret/application/dto/found-secret.dto.ts
@@ -0,0 +1,30 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class FoundSecretDto {
+  @ApiProperty()
+  id: string;
+
+  @ApiProperty()
+  slug: string;
+
+  @ApiProperty({ required: false })
+  value?: string;
+
+  @ApiProperty()
+  companyId: string;
+
+  @ApiProperty()
+  createdAt: Date;
+
+  @ApiProperty()
+  updatedAt: Date;
+
+  @ApiProperty({ required: false })
+  lastAccessedAt?: Date;
+
+  @ApiProperty({ required: false })
+  expiresAt?: Date;
+
+  @ApiProperty()
+  masterEncryption: boolean;
+}
diff --git a/backend/src/entities/user-secret/application/dto/secret-list.dto.ts b/backend/src/entities/user-secret/application/dto/secret-list.dto.ts
new file mode 100644
index 000000000..4e1568012
--- /dev/null
+++ b/backend/src/entities/user-secret/application/dto/secret-list.dto.ts
@@ -0,0 +1,40 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class SecretListItemDto {
+  @ApiProperty()
+  id: string;
+
+  @ApiProperty()
+  slug: string;
+
+  @ApiProperty()
+  companyId: string;
+
+  @ApiProperty()
+  createdAt: Date;
+
+  @ApiProperty()
+  updatedAt: Date;
+
+  @ApiProperty({ required: false })
+  lastAccessedAt?: Date;
+
+  @ApiProperty({ required: false })
+  expiresAt?: Date;
+
+  @ApiProperty()
+  masterEncryption: boolean;
+}
+
+export class SecretListResponseDto {
+  @ApiProperty({ type: [SecretListItemDto] })
+  data: SecretListItemDto[];
+
+  @ApiProperty()
+  pagination: {
+    total: number;
+    page: number;
+    limit: number;
+    totalPages: number;
+  };
+}
diff --git a/backend/src/entities/user-secret/application/dto/update-secret.dto.ts b/backend/src/entities/user-secret/application/dto/update-secret.dto.ts
new file mode 100644
index 000000000..91dc16596
--- /dev/null
+++ b/backend/src/entities/user-secret/application/dto/update-secret.dto.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsISO8601, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
+
+export class UpdateSecretDto {
+  @ApiProperty({ required: true, type: 'string' })
+  @IsString()
+  @IsNotEmpty()
+  @MinLength(1)
+  @MaxLength(10000)
+  value: string;
+
+  @ApiProperty({ required: false, type: 'string' })
+  @IsOptional()
+  @IsISO8601()
+  expiresAt?: string;
+}
diff --git a/backend/src/entities/user-secret/user-secret.controller.ts b/backend/src/entities/user-secret/user-secret.controller.ts
new file mode 100644
index 000000000..e60d17bf6
--- /dev/null
+++ b/backend/src/entities/user-secret/user-secret.controller.ts
@@ -0,0 +1,171 @@
+import {
+  Body,
+  Controller,
+  Delete,
+  Get,
+  HttpCode,
+  HttpStatus,
+  Injectable,
+  Param,
+  Post,
+  Put,
+  Query,
+  UseGuards,
+  UseInterceptors,
+} from '@nestjs/common';
+import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
+import { UserId } from '../../decorators/user-id.decorator.js';
+import { MasterPassword } from '../../decorators/master-password.decorator.js';
+import { SentryInterceptor } from '../../interceptors/sentry.interceptor.js';
+import { CreateSecretDto } from './application/dto/create-secret.dto.js';
+import { UpdateSecretDto } from './application/dto/update-secret.dto.js';
+import { FoundSecretDto } from './application/dto/found-secret.dto.js';
+import { SecretListResponseDto } from './application/dto/secret-list.dto.js';
+import { AuditLogResponseDto } from './application/dto/audit-log.dto.js';
+import { UserSecretsService } from './user-secrets.service.js';
+
+@UseInterceptors(SentryInterceptor)
+@Controller()
+@ApiBearerAuth()
+@ApiTags('Secrets')
+@Injectable()
+export class UserSecretController {
+  constructor(private readonly userSecretsService: UserSecretsService) {}
+
+  @ApiOperation({ summary: 'Create new secret' })
+  @ApiResponse({
+    status: 201,
+    description: 'Secret created successfully.',
+    type: FoundSecretDto,
+  })
+  @ApiResponse({
+    status: 409,
+    description: 'Secret with this slug already exists in your company.',
+  })
+  @Post('/secrets')
+  @HttpCode(HttpStatus.CREATED)
+  async createSecret(@UserId() userId: string, @Body() createDto: CreateSecretDto): Promise {
+    return await this.userSecretsService.createSecret(userId, createDto);
+  }
+
+  @ApiOperation({ summary: 'Get all company secrets' })
+  @ApiResponse({
+    status: 200,
+    description: 'Returns list of company secrets.',
+    type: SecretListResponseDto,
+  })
+  @ApiQuery({ name: 'page', required: false, type: Number })
+  @ApiQuery({ name: 'limit', required: false, type: Number })
+  @ApiQuery({ name: 'search', required: false, type: String })
+  @Get('/secrets')
+  async getSecrets(
+    @UserId() userId: string,
+    @Query('page') page?: number,
+    @Query('limit') limit?: number,
+    @Query('search') search?: string,
+  ): Promise {
+    return await this.userSecretsService.getSecrets(userId, {
+      page: page || 1,
+      limit: limit || 20,
+      search,
+    });
+  }
+
+  @ApiOperation({ summary: 'Get secret by slug' })
+  @ApiResponse({
+    status: 200,
+    description: 'Returns secret details with decrypted value.',
+    type: FoundSecretDto,
+  })
+  @ApiResponse({
+    status: 403,
+    description: 'Master password required or incorrect.',
+  })
+  @ApiResponse({
+    status: 404,
+    description: 'Secret not found.',
+  })
+  @ApiResponse({
+    status: 410,
+    description: 'Secret has expired.',
+  })
+  @Get('/secrets/:slug')
+  async getSecretBySlug(
+    @UserId() userId: string,
+    @Param('slug') slug: string,
+    @MasterPassword() masterPassword?: string,
+  ): Promise {
+    return await this.userSecretsService.getSecretBySlug(userId, slug, masterPassword);
+  }
+
+  @ApiOperation({ summary: 'Update secret' })
+  @ApiResponse({
+    status: 200,
+    description: 'Secret updated successfully.',
+    type: FoundSecretDto,
+  })
+  @ApiResponse({
+    status: 403,
+    description: 'You don\'t have permission to modify this secret.',
+  })
+  @ApiResponse({
+    status: 404,
+    description: 'Secret not found.',
+  })
+  @Put('/secrets/:slug')
+  async updateSecret(
+    @UserId() userId: string,
+    @Param('slug') slug: string,
+    @Body() updateDto: UpdateSecretDto,
+    @MasterPassword() masterPassword?: string,
+  ): Promise {
+    return await this.userSecretsService.updateSecret(userId, slug, updateDto, masterPassword);
+  }
+
+  @ApiOperation({ summary: 'Delete secret' })
+  @ApiResponse({
+    status: 200,
+    description: 'Secret deleted successfully.',
+  })
+  @ApiResponse({
+    status: 403,
+    description: 'You don\'t have permission to delete this secret.',
+  })
+  @ApiResponse({
+    status: 404,
+    description: 'Secret not found.',
+  })
+  @Delete('/secrets/:slug')
+  async deleteSecret(@UserId() userId: string, @Param('slug') slug: string): Promise<{ message: string; deletedAt: Date }> {
+    return await this.userSecretsService.deleteSecret(userId, slug);
+  }
+
+  @ApiOperation({ summary: 'Get secret audit log' })
+  @ApiResponse({
+    status: 200,
+    description: 'Returns audit log for the secret.',
+    type: AuditLogResponseDto,
+  })
+  @ApiResponse({
+    status: 403,
+    description: 'You don\'t have permission to view this audit log.',
+  })
+  @ApiResponse({
+    status: 404,
+    description: 'Secret not found.',
+  })
+  @ApiQuery({ name: 'page', required: false, type: Number })
+  @ApiQuery({ name: 'limit', required: false, type: Number })
+  @Get('/secrets/:slug/audit-log')
+  async getAuditLog(
+    @UserId() userId: string,
+    @Param('slug') slug: string,
+    @Query('page') page?: number,
+    @Query('limit') limit?: number,
+  ): Promise {
+    return await this.userSecretsService.getAuditLog(userId, slug, {
+      page: page || 1,
+      limit: limit || 50,
+    });
+  }
+}
diff --git a/backend/src/entities/user-secret/user-secret.entity.ts b/backend/src/entities/user-secret/user-secret.entity.ts
new file mode 100644
index 000000000..1b6ae3299
--- /dev/null
+++ b/backend/src/entities/user-secret/user-secret.entity.ts
@@ -0,0 +1,56 @@
+import {
+  Column,
+  CreateDateColumn,
+  Entity,
+  Index,
+  JoinColumn,
+  ManyToOne,
+  OneToMany,
+  PrimaryGeneratedColumn,
+  Relation,
+  UpdateDateColumn,
+} from 'typeorm';
+import { CompanyInfoEntity } from '../company-info/company-info.entity.js';
+import { SecretAccessLogEntity } from '../secret-access-log/secret-access-log.entity.js';
+
+@Entity('user_secrets')
+@Index(['companyId', 'slug'], { unique: true })
+export class UserSecretEntity {
+  @PrimaryGeneratedColumn('uuid')
+  id: string;
+
+  @ManyToOne(() => CompanyInfoEntity, { onDelete: 'CASCADE' })
+  @JoinColumn()
+  company: Relation;
+
+  @Column()
+  @Index()
+  companyId: string;
+
+  @Column({ type: 'varchar', length: 255 })
+  slug: string;
+
+  @Column({ type: 'text' })
+  encryptedValue: string;
+
+  @CreateDateColumn()
+  createdAt: Date;
+
+  @UpdateDateColumn()
+  updatedAt: Date;
+
+  @Column({ type: 'timestamp', nullable: true })
+  lastAccessedAt: Date;
+
+  @Column({ type: 'timestamp', nullable: true })
+  expiresAt: Date;
+
+  @Column({ default: false })
+  masterEncryption: boolean;
+
+  @Column({ type: 'varchar', length: 255, nullable: true })
+  masterHash: string;
+
+  @OneToMany(() => SecretAccessLogEntity, (log) => log.secret)
+  accessLogs: Relation[];
+}
diff --git a/backend/src/entities/user-secret/user-secret.module.ts b/backend/src/entities/user-secret/user-secret.module.ts
new file mode 100644
index 000000000..d659dbc5d
--- /dev/null
+++ b/backend/src/entities/user-secret/user-secret.module.ts
@@ -0,0 +1,15 @@
+import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { UserSecretEntity } from './user-secret.entity.js';
+import { SecretAccessLogEntity } from '../secret-access-log/secret-access-log.entity.js';
+import { UserEntity } from '../user/user.entity.js';
+import { UserSecretController } from './user-secret.controller.js';
+import { UserSecretsService } from './user-secrets.service.js';
+
+@Module({
+  imports: [TypeOrmModule.forFeature([UserSecretEntity, SecretAccessLogEntity, UserEntity])],
+  providers: [UserSecretsService],
+  controllers: [UserSecretController],
+  exports: [UserSecretsService],
+})
+export class UserSecretModule {}
diff --git a/backend/src/entities/user-secret/user-secrets.service.spec.ts b/backend/src/entities/user-secret/user-secrets.service.spec.ts
new file mode 100644
index 000000000..717906577
--- /dev/null
+++ b/backend/src/entities/user-secret/user-secrets.service.spec.ts
@@ -0,0 +1,280 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { Repository } from 'typeorm';
+import { getRepositoryToken } from '@nestjs/typeorm';
+import { ConflictException, ForbiddenException, GoneException, NotFoundException } from '@nestjs/common';
+import { UserSecretsService } from './user-secrets.service.js';
+import { UserSecretEntity } from './user-secret.entity.js';
+import { SecretAccessLogEntity, SecretActionEnum } from '../secret-access-log/secret-access-log.entity.js';
+import { UserEntity } from '../user/user.entity.js';
+import { CompanyInfoEntity } from '../company-info/company-info.entity.js';
+
+describe('UserSecretsService', () => {
+  let service: UserSecretsService;
+  let secretRepository: Repository;
+  let auditLogRepository: Repository;
+  let userRepository: Repository;
+
+  const mockCompany: CompanyInfoEntity = {
+    id: 'company-uuid-1',
+    name: 'Test Company',
+  } as CompanyInfoEntity;
+
+  const mockUser: UserEntity = {
+    id: 'user-uuid-1',
+    email: 'test@example.com',
+    company: mockCompany,
+  } as UserEntity;
+
+  const mockSecret: UserSecretEntity = {
+    id: 'secret-uuid-1',
+    slug: 'test-secret',
+    encryptedValue: 'encrypted-value',
+    companyId: 'company-uuid-1',
+    company: mockCompany,
+    createdAt: new Date('2025-01-01'),
+    updatedAt: new Date('2025-01-01'),
+    lastAccessedAt: null,
+    expiresAt: null,
+    masterEncryption: false,
+    masterHash: null,
+    accessLogs: [],
+  };
+
+  beforeEach(async () => {
+    const module: TestingModule = await Test.createTestingModule({
+      providers: [
+        UserSecretsService,
+        {
+          provide: getRepositoryToken(UserSecretEntity),
+          useValue: {
+            findOne: jest.fn(),
+            findAndCount: jest.fn(),
+            create: jest.fn(),
+            save: jest.fn(),
+            remove: jest.fn(),
+          },
+        },
+        {
+          provide: getRepositoryToken(SecretAccessLogEntity),
+          useValue: {
+            create: jest.fn(),
+            save: jest.fn(),
+            findAndCount: jest.fn(),
+          },
+        },
+        {
+          provide: getRepositoryToken(UserEntity),
+          useValue: {
+            findOne: jest.fn(),
+          },
+        },
+      ],
+    }).compile();
+
+    service = module.get(UserSecretsService);
+    secretRepository = module.get>(getRepositoryToken(UserSecretEntity));
+    auditLogRepository = module.get>(getRepositoryToken(SecretAccessLogEntity));
+    userRepository = module.get>(getRepositoryToken(UserEntity));
+  });
+
+  describe('createSecret', () => {
+    it('should create a new secret successfully', async () => {
+      const createDto = {
+        slug: 'new-secret',
+        value: 'secret-value',
+        masterEncryption: false,
+      };
+
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(null); // No existing secret
+      jest.spyOn(secretRepository, 'create').mockReturnValue(mockSecret);
+      jest.spyOn(secretRepository, 'save').mockResolvedValue(mockSecret);
+      jest.spyOn(auditLogRepository, 'create').mockReturnValue({} as SecretAccessLogEntity);
+      jest.spyOn(auditLogRepository, 'save').mockResolvedValue({} as SecretAccessLogEntity);
+
+      const result = await service.createSecret('user-uuid-1', createDto);
+
+      expect(result.slug).toBe('test-secret');
+      expect(result.companyId).toBe('company-uuid-1');
+      expect(userRepository.findOne).toHaveBeenCalledWith({
+        where: { id: 'user-uuid-1' },
+        relations: ['company'],
+      });
+    });
+
+    it('should throw ConflictException if slug already exists', async () => {
+      const createDto = {
+        slug: 'existing-secret',
+        value: 'secret-value',
+      };
+
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(mockSecret); // Existing secret
+
+      await expect(service.createSecret('user-uuid-1', createDto)).rejects.toThrow(ConflictException);
+    });
+
+    it('should throw ForbiddenException if user has no company', async () => {
+      const createDto = {
+        slug: 'new-secret',
+        value: 'secret-value',
+      };
+
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue({ ...mockUser, company: null } as UserEntity);
+
+      await expect(service.createSecret('user-uuid-1', createDto)).rejects.toThrow(ForbiddenException);
+    });
+  });
+
+  describe('getSecretBySlug', () => {
+    it('should return secret with decrypted value', async () => {
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(mockSecret);
+      jest.spyOn(secretRepository, 'save').mockResolvedValue(mockSecret);
+      jest.spyOn(auditLogRepository, 'create').mockReturnValue({} as SecretAccessLogEntity);
+      jest.spyOn(auditLogRepository, 'save').mockResolvedValue({} as SecretAccessLogEntity);
+
+      const result = await service.getSecretBySlug('user-uuid-1', 'test-secret');
+
+      expect(result.slug).toBe('test-secret');
+      expect(result.value).toBe('encrypted-value'); // TODO: Should be decrypted
+      expect(auditLogRepository.save).toHaveBeenCalled();
+    });
+
+    it('should throw NotFoundException if secret not found', async () => {
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(null);
+
+      await expect(service.getSecretBySlug('user-uuid-1', 'non-existent')).rejects.toThrow(NotFoundException);
+    });
+
+    it('should throw GoneException if secret is expired', async () => {
+      const expiredSecret = { ...mockSecret, expiresAt: new Date('2020-01-01') };
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(expiredSecret);
+
+      await expect(service.getSecretBySlug('user-uuid-1', 'test-secret')).rejects.toThrow(GoneException);
+    });
+
+    it('should throw ForbiddenException if master password required but not provided', async () => {
+      const protectedSecret = { ...mockSecret, masterEncryption: true };
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(protectedSecret);
+
+      await expect(service.getSecretBySlug('user-uuid-1', 'test-secret')).rejects.toThrow(ForbiddenException);
+    });
+  });
+
+  describe('updateSecret', () => {
+    it('should update secret value successfully', async () => {
+      const updateDto = {
+        value: 'new-value',
+      };
+
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(mockSecret);
+      jest.spyOn(secretRepository, 'save').mockResolvedValue({ ...mockSecret, encryptedValue: 'new-value' });
+      jest.spyOn(auditLogRepository, 'create').mockReturnValue({} as SecretAccessLogEntity);
+      jest.spyOn(auditLogRepository, 'save').mockResolvedValue({} as SecretAccessLogEntity);
+
+      const result = await service.updateSecret('user-uuid-1', 'test-secret', updateDto);
+
+      expect(secretRepository.save).toHaveBeenCalled();
+      expect(auditLogRepository.save).toHaveBeenCalled();
+    });
+
+    it('should throw NotFoundException if secret not found', async () => {
+      const updateDto = { value: 'new-value' };
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(null);
+
+      await expect(service.updateSecret('user-uuid-1', 'non-existent', updateDto)).rejects.toThrow(NotFoundException);
+    });
+  });
+
+  describe('deleteSecret', () => {
+    it('should delete secret successfully', async () => {
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(mockSecret);
+      jest.spyOn(secretRepository, 'remove').mockResolvedValue(mockSecret);
+      jest.spyOn(auditLogRepository, 'create').mockReturnValue({} as SecretAccessLogEntity);
+      jest.spyOn(auditLogRepository, 'save').mockResolvedValue({} as SecretAccessLogEntity);
+
+      const result = await service.deleteSecret('user-uuid-1', 'test-secret');
+
+      expect(result.message).toBe('Secret deleted successfully');
+      expect(secretRepository.remove).toHaveBeenCalledWith(mockSecret);
+      expect(auditLogRepository.save).toHaveBeenCalled();
+    });
+
+    it('should throw NotFoundException if secret not found', async () => {
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(null);
+
+      await expect(service.deleteSecret('user-uuid-1', 'non-existent')).rejects.toThrow(NotFoundException);
+    });
+  });
+
+  describe('getSecrets', () => {
+    it('should return paginated list of secrets', async () => {
+      const mockSecrets = [mockSecret];
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findAndCount').mockResolvedValue([mockSecrets, 1]);
+
+      const result = await service.getSecrets('user-uuid-1', { page: 1, limit: 20 });
+
+      expect(result.data).toHaveLength(1);
+      expect(result.pagination.total).toBe(1);
+      expect(result.pagination.page).toBe(1);
+      expect(result.pagination.totalPages).toBe(1);
+    });
+
+    it('should filter secrets by search term', async () => {
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findAndCount').mockResolvedValue([[], 0]);
+
+      await service.getSecrets('user-uuid-1', { page: 1, limit: 20, search: 'test' });
+
+      expect(secretRepository.findAndCount).toHaveBeenCalledWith(
+        expect.objectContaining({
+          where: expect.objectContaining({
+            slug: expect.anything(),
+          }),
+        }),
+      );
+    });
+  });
+
+  describe('getAuditLog', () => {
+    it('should return paginated audit log', async () => {
+      const mockLog: SecretAccessLogEntity = {
+        id: 'log-uuid-1',
+        secretId: 'secret-uuid-1',
+        userId: 'user-uuid-1',
+        user: mockUser,
+        action: SecretActionEnum.VIEW,
+        accessedAt: new Date(),
+        success: true,
+      } as SecretAccessLogEntity;
+
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(mockSecret);
+      jest.spyOn(auditLogRepository, 'findAndCount').mockResolvedValue([[mockLog], 1]);
+
+      const result = await service.getAuditLog('user-uuid-1', 'test-secret', { page: 1, limit: 50 });
+
+      expect(result.data).toHaveLength(1);
+      expect(result.data[0].action).toBe(SecretActionEnum.VIEW);
+      expect(result.pagination.total).toBe(1);
+    });
+
+    it('should throw NotFoundException if secret not found', async () => {
+      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
+      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(null);
+
+      await expect(service.getAuditLog('user-uuid-1', 'non-existent', { page: 1, limit: 50 })).rejects.toThrow(
+        NotFoundException,
+      );
+    });
+  });
+});
diff --git a/backend/src/entities/user-secret/user-secrets.service.ts b/backend/src/entities/user-secret/user-secrets.service.ts
new file mode 100644
index 000000000..5cf7a3196
--- /dev/null
+++ b/backend/src/entities/user-secret/user-secrets.service.ts
@@ -0,0 +1,324 @@
+import {
+  ConflictException,
+  ForbiddenException,
+  GoneException,
+  Injectable,
+  NotFoundException,
+} from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository, Like } from 'typeorm';
+import { UserSecretEntity } from './user-secret.entity.js';
+import { SecretAccessLogEntity, SecretActionEnum } from '../secret-access-log/secret-access-log.entity.js';
+import { UserEntity } from '../user/user.entity.js';
+import { CreateSecretDto } from './application/dto/create-secret.dto.js';
+import { UpdateSecretDto } from './application/dto/update-secret.dto.js';
+import { FoundSecretDto } from './application/dto/found-secret.dto.js';
+import { SecretListResponseDto, SecretListItemDto } from './application/dto/secret-list.dto.js';
+import { AuditLogResponseDto, AuditLogEntryDto } from './application/dto/audit-log.dto.js';
+
+@Injectable()
+export class UserSecretsService {
+  constructor(
+    @InjectRepository(UserSecretEntity)
+    private readonly secretRepository: Repository,
+    @InjectRepository(SecretAccessLogEntity)
+    private readonly auditLogRepository: Repository,
+    @InjectRepository(UserEntity)
+    private readonly userRepository: Repository,
+  ) {}
+
+  private async getUserWithCompany(userId: string): Promise {
+    const user = await this.userRepository.findOne({
+      where: { id: userId },
+      relations: ['company'],
+    });
+
+    if (!user || !user.company) {
+      throw new ForbiddenException('User not found or not associated with a company');
+    }
+
+    return user;
+  }
+
+  private async logAccess(
+    secretId: string,
+    userId: string,
+    action: SecretActionEnum,
+    success: boolean = true,
+    errorMessage?: string,
+  ): Promise {
+    const log = this.auditLogRepository.create({
+      secretId,
+      userId,
+      action,
+      success,
+      errorMessage,
+      accessedAt: new Date(),
+    });
+
+    await this.auditLogRepository.save(log);
+  }
+
+  async createSecret(userId: string, createDto: CreateSecretDto): Promise {
+    const user = await this.getUserWithCompany(userId);
+
+    // Check if slug already exists in company
+    const existing = await this.secretRepository.findOne({
+      where: {
+        slug: createDto.slug,
+        companyId: user.company.id,
+      },
+    });
+
+    if (existing) {
+      throw new ConflictException('Secret with this slug already exists in your company');
+    }
+
+    // Create secret
+    const secret = this.secretRepository.create({
+      slug: createDto.slug,
+      encryptedValue: createDto.value, // TODO: Encrypt with PRIVATE_KEY and optionally master password
+      companyId: user.company.id,
+      expiresAt: createDto.expiresAt ? new Date(createDto.expiresAt) : null,
+      masterEncryption: createDto.masterEncryption || false,
+      masterHash: createDto.masterPassword ? 'TODO: Hash master password' : null, // TODO: Hash master password
+    });
+
+    const saved = await this.secretRepository.save(secret);
+
+    // Log creation
+    await this.logAccess(saved.id, userId, SecretActionEnum.CREATE);
+
+    return this.buildFoundSecretDto(saved, false);
+  }
+
+  async getSecrets(
+    userId: string,
+    options: { page: number; limit: number; search?: string },
+  ): Promise {
+    const user = await this.getUserWithCompany(userId);
+
+    const where: any = {
+      companyId: user.company.id,
+    };
+
+    if (options.search) {
+      where.slug = Like(`%${options.search}%`);
+    }
+
+    const [secrets, total] = await this.secretRepository.findAndCount({
+      where,
+      order: {
+        createdAt: 'DESC',
+      },
+      skip: (options.page - 1) * options.limit,
+      take: options.limit,
+    });
+
+    return {
+      data: secrets.map((secret) => this.buildSecretListItemDto(secret)),
+      pagination: {
+        total,
+        page: options.page,
+        limit: options.limit,
+        totalPages: Math.ceil(total / options.limit),
+      },
+    };
+  }
+
+  async getSecretBySlug(userId: string, slug: string, masterPassword?: string): Promise {
+    const user = await this.getUserWithCompany(userId);
+
+    const secret = await this.secretRepository.findOne({
+      where: {
+        slug,
+        companyId: user.company.id,
+      },
+    });
+
+    if (!secret) {
+      throw new NotFoundException('Secret not found');
+    }
+
+    // Check expiration
+    if (secret.expiresAt && secret.expiresAt < new Date()) {
+      throw new GoneException('Secret has expired');
+    }
+
+    // Check master password if required
+    if (secret.masterEncryption && !masterPassword) {
+      throw new ForbiddenException('Master password required');
+    }
+
+    if (secret.masterEncryption && masterPassword) {
+      // TODO: Verify master password
+      const isValid = true; // TODO: Verify against masterHash
+      if (!isValid) {
+        await this.logAccess(secret.id, userId, SecretActionEnum.VIEW, false, 'Invalid master password');
+        throw new ForbiddenException('Invalid master password');
+      }
+    }
+
+    // Update last accessed
+    secret.lastAccessedAt = new Date();
+    await this.secretRepository.save(secret);
+
+    // Log access
+    await this.logAccess(secret.id, userId, SecretActionEnum.VIEW);
+
+    return this.buildFoundSecretDto(secret, true);
+  }
+
+  async updateSecret(
+    userId: string,
+    slug: string,
+    updateDto: UpdateSecretDto,
+    masterPassword?: string,
+  ): Promise {
+    const user = await this.getUserWithCompany(userId);
+
+    const secret = await this.secretRepository.findOne({
+      where: {
+        slug,
+        companyId: user.company.id,
+      },
+    });
+
+    if (!secret) {
+      throw new NotFoundException('Secret not found');
+    }
+
+    // Check expiration
+    if (secret.expiresAt && secret.expiresAt < new Date()) {
+      throw new GoneException('Secret has expired');
+    }
+
+    // Check master password if required
+    if (secret.masterEncryption && !masterPassword) {
+      throw new ForbiddenException('Master password required');
+    }
+
+    // Update secret
+    secret.encryptedValue = updateDto.value; // TODO: Encrypt
+    if (updateDto.expiresAt !== undefined) {
+      secret.expiresAt = updateDto.expiresAt ? new Date(updateDto.expiresAt) : null;
+    }
+
+    const updated = await this.secretRepository.save(secret);
+
+    // Log update
+    await this.logAccess(secret.id, userId, SecretActionEnum.UPDATE);
+
+    return this.buildFoundSecretDto(updated, false);
+  }
+
+  async deleteSecret(userId: string, slug: string): Promise<{ message: string; deletedAt: Date }> {
+    const user = await this.getUserWithCompany(userId);
+
+    const secret = await this.secretRepository.findOne({
+      where: {
+        slug,
+        companyId: user.company.id,
+      },
+    });
+
+    if (!secret) {
+      throw new NotFoundException('Secret not found');
+    }
+
+    // Log deletion before deleting
+    await this.logAccess(secret.id, userId, SecretActionEnum.DELETE);
+
+    // Permanently delete
+    await this.secretRepository.remove(secret);
+
+    return {
+      message: 'Secret deleted successfully',
+      deletedAt: new Date(),
+    };
+  }
+
+  async getAuditLog(
+    userId: string,
+    slug: string,
+    options: { page: number; limit: number },
+  ): Promise {
+    const user = await this.getUserWithCompany(userId);
+
+    const secret = await this.secretRepository.findOne({
+      where: {
+        slug,
+        companyId: user.company.id,
+      },
+    });
+
+    if (!secret) {
+      throw new NotFoundException('Secret not found');
+    }
+
+    const [logs, total] = await this.auditLogRepository.findAndCount({
+      where: {
+        secretId: secret.id,
+      },
+      relations: ['user'],
+      order: {
+        accessedAt: 'DESC',
+      },
+      skip: (options.page - 1) * options.limit,
+      take: options.limit,
+    });
+
+    return {
+      data: logs.map((log) => this.buildAuditLogEntryDto(log)),
+      pagination: {
+        total,
+        page: options.page,
+        limit: options.limit,
+        totalPages: Math.ceil(total / options.limit),
+      },
+    };
+  }
+
+  private buildFoundSecretDto(secret: UserSecretEntity, includeValue: boolean): FoundSecretDto {
+    return {
+      id: secret.id,
+      slug: secret.slug,
+      value: includeValue ? secret.encryptedValue : undefined, // TODO: Decrypt
+      companyId: secret.companyId,
+      createdAt: secret.createdAt,
+      updatedAt: secret.updatedAt,
+      lastAccessedAt: secret.lastAccessedAt,
+      expiresAt: secret.expiresAt,
+      masterEncryption: secret.masterEncryption,
+    };
+  }
+
+  private buildSecretListItemDto(secret: UserSecretEntity): SecretListItemDto {
+    return {
+      id: secret.id,
+      slug: secret.slug,
+      companyId: secret.companyId,
+      createdAt: secret.createdAt,
+      updatedAt: secret.updatedAt,
+      lastAccessedAt: secret.lastAccessedAt,
+      expiresAt: secret.expiresAt,
+      masterEncryption: secret.masterEncryption,
+    };
+  }
+
+  private buildAuditLogEntryDto(log: SecretAccessLogEntity): AuditLogEntryDto {
+    return {
+      id: log.id,
+      action: log.action,
+      user: {
+        id: log.user.id,
+        email: log.user.email,
+      },
+      accessedAt: log.accessedAt,
+      ipAddress: log.ipAddress,
+      userAgent: log.userAgent,
+      success: log.success,
+      errorMessage: log.errorMessage,
+    };
+  }
+}
diff --git a/backend/src/migrations/1763724061000-CreateUserSecretEntity.ts b/backend/src/migrations/1763724061000-CreateUserSecretEntity.ts
new file mode 100644
index 000000000..2c399b1cb
--- /dev/null
+++ b/backend/src/migrations/1763724061000-CreateUserSecretEntity.ts
@@ -0,0 +1,38 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class CreateUserSecretEntity1763724061000 implements MigrationInterface {
+    name = 'CreateUserSecretEntity1763724061000'
+
+    public async up(queryRunner: QueryRunner): Promise {
+        await queryRunner.query(`CREATE TABLE "user_secrets" (
+            "id" uuid NOT NULL DEFAULT uuid_generate_v4(),
+            "companyId" uuid NOT NULL,
+            "slug" character varying(255) NOT NULL,
+            "encryptedValue" text NOT NULL,
+            "createdAt" TIMESTAMP NOT NULL DEFAULT now(),
+            "updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
+            "lastAccessedAt" TIMESTAMP,
+            "expiresAt" TIMESTAMP,
+            "masterEncryption" boolean NOT NULL DEFAULT false,
+            "masterHash" character varying(255),
+            CONSTRAINT "PK_user_secrets" PRIMARY KEY ("id")
+        )`);
+
+        await queryRunner.query(`CREATE INDEX "IDX_user_secrets_companyId" ON "user_secrets" ("companyId")`);
+        await queryRunner.query(`CREATE INDEX "IDX_user_secrets_createdAt" ON "user_secrets" ("createdAt")`);
+        await queryRunner.query(`CREATE INDEX "IDX_user_secrets_expiresAt" ON "user_secrets" ("expiresAt")`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_user_secrets_company_slug" ON "user_secrets" ("companyId", "slug")`);
+
+        await queryRunner.query(`ALTER TABLE "user_secrets" ADD CONSTRAINT "FK_user_secrets_companyId" FOREIGN KEY ("companyId") REFERENCES "company_info"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise {
+        await queryRunner.query(`ALTER TABLE "user_secrets" DROP CONSTRAINT "FK_user_secrets_companyId"`);
+        await queryRunner.query(`DROP INDEX "IDX_user_secrets_company_slug"`);
+        await queryRunner.query(`DROP INDEX "IDX_user_secrets_expiresAt"`);
+        await queryRunner.query(`DROP INDEX "IDX_user_secrets_createdAt"`);
+        await queryRunner.query(`DROP INDEX "IDX_user_secrets_companyId"`);
+        await queryRunner.query(`DROP TABLE "user_secrets"`);
+    }
+
+}
diff --git a/backend/src/migrations/1763724062000-CreateSecretAccessLogEntity.ts b/backend/src/migrations/1763724062000-CreateSecretAccessLogEntity.ts
new file mode 100644
index 000000000..48fb6c399
--- /dev/null
+++ b/backend/src/migrations/1763724062000-CreateSecretAccessLogEntity.ts
@@ -0,0 +1,40 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class CreateSecretAccessLogEntity1763724062000 implements MigrationInterface {
+    name = 'CreateSecretAccessLogEntity1763724062000'
+
+    public async up(queryRunner: QueryRunner): Promise {
+        await queryRunner.query(`CREATE TYPE "secret_access_logs_action_enum" AS ENUM('create', 'view', 'copy', 'update', 'delete')`);
+
+        await queryRunner.query(`CREATE TABLE "secret_access_logs" (
+            "id" uuid NOT NULL DEFAULT uuid_generate_v4(),
+            "secretId" uuid NOT NULL,
+            "userId" uuid NOT NULL,
+            "action" "secret_access_logs_action_enum" NOT NULL,
+            "accessedAt" TIMESTAMP NOT NULL DEFAULT now(),
+            "ipAddress" character varying(45),
+            "userAgent" text,
+            "success" boolean NOT NULL DEFAULT true,
+            "errorMessage" text,
+            CONSTRAINT "PK_secret_access_logs" PRIMARY KEY ("id")
+        )`);
+
+        await queryRunner.query(`CREATE INDEX "IDX_secret_access_logs_secretId" ON "secret_access_logs" ("secretId")`);
+        await queryRunner.query(`CREATE INDEX "IDX_secret_access_logs_userId" ON "secret_access_logs" ("userId")`);
+        await queryRunner.query(`CREATE INDEX "IDX_secret_access_logs_accessedAt" ON "secret_access_logs" ("accessedAt")`);
+
+        await queryRunner.query(`ALTER TABLE "secret_access_logs" ADD CONSTRAINT "FK_secret_access_logs_secretId" FOREIGN KEY ("secretId") REFERENCES "user_secrets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "secret_access_logs" ADD CONSTRAINT "FK_secret_access_logs_userId" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise {
+        await queryRunner.query(`ALTER TABLE "secret_access_logs" DROP CONSTRAINT "FK_secret_access_logs_userId"`);
+        await queryRunner.query(`ALTER TABLE "secret_access_logs" DROP CONSTRAINT "FK_secret_access_logs_secretId"`);
+        await queryRunner.query(`DROP INDEX "IDX_secret_access_logs_accessedAt"`);
+        await queryRunner.query(`DROP INDEX "IDX_secret_access_logs_userId"`);
+        await queryRunner.query(`DROP INDEX "IDX_secret_access_logs_secretId"`);
+        await queryRunner.query(`DROP TABLE "secret_access_logs"`);
+        await queryRunner.query(`DROP TYPE "secret_access_logs_action_enum"`);
+    }
+
+}
diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
new file mode 100644
index 000000000..f271a6991
--- /dev/null
+++ b/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
@@ -0,0 +1,659 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import { faker } from '@faker-js/faker';
+import { INestApplication, ValidationPipe } from '@nestjs/common';
+import { Test } from '@nestjs/testing';
+import test from 'ava';
+import cookieParser from 'cookie-parser';
+import request from 'supertest';
+import { ApplicationModule } from '../../../src/app.module.js';
+import { AllExceptionsFilter } from '../../../src/exceptions/all-exceptions.filter.js';
+import { Messages } from '../../../src/exceptions/text/messages.js';
+import { Cacher } from '../../../src/helpers/cache/cacher.js';
+import { DatabaseModule } from '../../../src/shared/database/database.module.js';
+import { DatabaseService } from '../../../src/shared/database/database.service.js';
+import { MockFactory } from '../../mock.factory.js';
+import { getTestData } from '../../utils/get-test-data.js';
+import { registerUserAndReturnUserInfo } from '../../utils/register-user-and-return-user-info.js';
+import { TestUtils } from '../../utils/test.utils.js';
+import { setSaasEnvVariable } from '../../utils/set-saas-env-variable.js';
+import { ValidationException } from '../../../src/exceptions/custom-exceptions/validation-exception.js';
+import { ValidationError } from 'class-validator';
+import { WinstonLogger } from '../../../src/entities/logging/winston-logger.js';
+
+const mockFactory = new MockFactory();
+let app: INestApplication;
+let testUtils: TestUtils;
+let currentTest: string;
+
+test.before(async () => {
+  setSaasEnvVariable();
+  const moduleFixture = await Test.createTestingModule({
+    imports: [ApplicationModule, DatabaseModule],
+    providers: [DatabaseService, TestUtils],
+  }).compile();
+  app = moduleFixture.createNestApplication();
+  testUtils = moduleFixture.get(TestUtils);
+
+  app.use(cookieParser());
+  app.useGlobalFilters(new AllExceptionsFilter(app.get(WinstonLogger)));
+  app.useGlobalPipes(
+    new ValidationPipe({
+      exceptionFactory(validationErrors: ValidationError[] = []) {
+        return new ValidationException(validationErrors);
+      },
+    }),
+  );
+  await app.init();
+  app.getHttpServer().listen(0);
+});
+
+test.after(async () => {
+  try {
+    await Cacher.clearAllCache();
+    await app.close();
+  } catch (e) {
+    console.error('After tests error ' + e);
+  }
+});
+
+currentTest = 'POST /secrets';
+test.serial(`${currentTest} - should create a new secret`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const createDto = {
+    slug: 'test-api-key',
+    value: 'sk-test-1234567890',
+    masterEncryption: false,
+  };
+
+  const response = await request(app.getHttpServer())
+    .post('/secrets')
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json')
+    .send(createDto);
+
+  t.is(response.status, 201);
+  const responseBody = JSON.parse(response.text);
+  t.is(responseBody.slug, 'test-api-key');
+  t.is(responseBody.masterEncryption, false);
+  t.truthy(responseBody.id);
+  t.truthy(responseBody.createdAt);
+  t.falsy(responseBody.value);
+});
+
+test.serial(`${currentTest} - should return 409 for duplicate slug`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const createDto = {
+    slug: 'test-api-key',
+    value: 'sk-another-key',
+  };
+
+  const response = await request(app.getHttpServer())
+    .post('/secrets')
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json')
+    .send(createDto);
+
+  t.is(response.status, 409);
+  const responseBody = JSON.parse(response.text);
+  t.truthy(responseBody.message);
+});
+
+test.serial(`${currentTest} - should validate slug format`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const createDto = {
+    slug: 'invalid slug with spaces!',
+    value: 'sk-test-key',
+  };
+
+  const response = await request(app.getHttpServer())
+    .post('/secrets')
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json')
+    .send(createDto);
+
+  t.is(response.status, 400);
+  const responseBody = JSON.parse(response.text);
+  t.truthy(responseBody.message);
+});
+
+currentTest = 'GET /secrets';
+test.serial(`${currentTest} - should return list of company secrets`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const response = await request(app.getHttpServer())
+    .get('/secrets')
+    .set('Cookie', token)
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 200);
+  const responseBody = JSON.parse(response.text);
+  t.truthy(responseBody.data);
+  t.true(Array.isArray(responseBody.data));
+  t.truthy(responseBody.pagination);
+  t.is(responseBody.pagination.page, 1);
+  t.true(responseBody.data.length > 0);
+
+  const firstSecret = responseBody.data[0];
+  t.falsy(firstSecret.value);
+  t.truthy(firstSecret.slug);
+});
+
+test.serial(`${currentTest}?search=test - should filter secrets by slug`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const response = await request(app.getHttpServer())
+    .get('/secrets?search=test')
+    .set('Cookie', token)
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 200);
+  const responseBody = JSON.parse(response.text);
+  t.truthy(responseBody.data);
+  t.true(responseBody.data.every((s: any) => s.slug.includes('test')));
+});
+
+currentTest = 'GET /secrets/:slug';
+test.serial(`${currentTest} - should return secret with value`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const response = await request(app.getHttpServer())
+    .get('/secrets/test-api-key')
+    .set('Cookie', token)
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 200);
+  const responseBody = JSON.parse(response.text);
+  t.is(responseBody.slug, 'test-api-key');
+  t.truthy(responseBody.value);
+  t.truthy(responseBody.lastAccessedAt);
+});
+
+test.serial(`${currentTest} - should return 404 for non-existent secret`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const response = await request(app.getHttpServer())
+    .get('/secrets/non-existent-secret')
+    .set('Cookie', token)
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 404);
+});
+
+currentTest = 'POST /secrets';
+test.serial(`${currentTest} - should create secret with master password`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const createDto = {
+    slug: 'protected-secret',
+    value: 'sensitive-data',
+    masterEncryption: true,
+    masterPassword: 'MasterPass123!',
+  };
+
+  const response = await request(app.getHttpServer())
+    .post('/secrets')
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json')
+    .send(createDto);
+
+  t.is(response.status, 201);
+  const responseBody = JSON.parse(response.text);
+  t.is(responseBody.slug, 'protected-secret');
+  t.is(responseBody.masterEncryption, true);
+});
+
+currentTest = 'GET /secrets/:slug';
+test.serial(`${currentTest} - should require master password for protected secret`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const response = await request(app.getHttpServer())
+    .get('/secrets/protected-secret')
+    .set('Cookie', token)
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 403);
+});
+
+test.serial(`${currentTest} - should return protected secret with correct master password`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const response = await request(app.getHttpServer())
+    .get('/secrets/protected-secret')
+    .set('Cookie', token)
+    .set('masterpwd', 'MasterPass123!')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 200);
+  const responseBody = JSON.parse(response.text);
+  t.is(responseBody.slug, 'protected-secret');
+  t.truthy(responseBody.value);
+});
+
+currentTest = 'PUT /secrets/:slug';
+test.serial(`${currentTest} - should update secret value`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const updateDto = {
+    value: 'updated-secret-value',
+  };
+
+  const response = await request(app.getHttpServer())
+    .put('/secrets/test-api-key')
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json')
+    .send(updateDto);
+
+  t.is(response.status, 200);
+  const responseBody = JSON.parse(response.text);
+  t.is(responseBody.slug, 'test-api-key');
+  t.truthy(responseBody.updatedAt);
+});
+
+test.serial(`${currentTest} - should update expiration date`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const futureDate = new Date();
+  futureDate.setFullYear(futureDate.getFullYear() + 1);
+
+  const updateDto = {
+    value: 'updated-secret-value',
+    expiresAt: futureDate.toISOString(),
+  };
+
+  const response = await request(app.getHttpServer())
+    .put('/secrets/test-api-key')
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json')
+    .send(updateDto);
+
+  t.is(response.status, 200);
+  const responseBody = JSON.parse(response.text);
+  t.truthy(responseBody.expiresAt);
+});
+
+test.serial(`${currentTest} - should return 404 for non-existent secret`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const updateDto = {
+    value: 'new-value',
+  };
+
+  const response = await request(app.getHttpServer())
+    .put('/secrets/non-existent')
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json')
+    .send(updateDto);
+
+  t.is(response.status, 404);
+});
+
+currentTest = 'GET /secrets/:slug/audit-log';
+test.serial(`${currentTest} - should return audit log entries`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const response = await request(app.getHttpServer())
+    .get('/secrets/test-api-key/audit-log')
+    .set('Cookie', token)
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 200);
+  const responseBody = JSON.parse(response.text);
+  t.truthy(responseBody.data);
+  t.true(Array.isArray(responseBody.data));
+  t.truthy(responseBody.pagination);
+
+  t.true(responseBody.data.length >= 3);
+
+  const logEntry = responseBody.data[0];
+  t.truthy(logEntry.id);
+  t.truthy(logEntry.action);
+  t.truthy(logEntry.user);
+  t.truthy(logEntry.accessedAt);
+  t.is(logEntry.success, true);
+});
+
+test.serial(`${currentTest}?page=1&limit=2 - should paginate audit log`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const response = await request(app.getHttpServer())
+    .get('/secrets/test-api-key/audit-log?page=1&limit=2')
+    .set('Cookie', token)
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 200);
+  const responseBody = JSON.parse(response.text);
+  t.is(responseBody.pagination.limit, 2);
+  t.true(responseBody.data.length <= 2);
+});
+
+currentTest = 'POST /secrets';
+test.serial(`${currentTest} - should create expired secret`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const pastDate = new Date();
+  pastDate.setFullYear(pastDate.getFullYear() - 1);
+
+  const createDto = {
+    slug: 'expired-secret',
+    value: 'expired-value',
+    expiresAt: pastDate.toISOString(),
+  };
+
+  const response = await request(app.getHttpServer())
+    .post('/secrets')
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json')
+    .send(createDto);
+
+  t.is(response.status, 201);
+});
+
+currentTest = 'GET /secrets/:slug';
+test.serial(`${currentTest} - should return 410 for expired secret`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const response = await request(app.getHttpServer())
+    .get('/secrets/expired-secret')
+    .set('Cookie', token)
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 410);
+});
+
+currentTest = 'DELETE /secrets/:slug';
+test.serial(`${currentTest} - should delete secret`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const response = await request(app.getHttpServer())
+    .delete('/secrets/test-api-key')
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 200);
+  const responseBody = JSON.parse(response.text);
+  t.is(responseBody.message, 'Secret deleted successfully');
+  t.truthy(responseBody.deletedAt);
+});
+
+currentTest = 'GET /secrets/:slug';
+test.serial(`${currentTest} - should return 404 after deletion`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const response = await request(app.getHttpServer())
+    .get('/secrets/test-api-key')
+    .set('Cookie', token)
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 404);
+});
+
+currentTest = 'DELETE /secrets/:slug';
+test.serial(`${currentTest} - should return 404 for non-existent secret`, async (t) => {
+  const { token } = await registerUserAndReturnUserInfo(app);
+  const newConnection = getTestData(mockFactory).newEncryptedConnection;
+  const createdConnection = await request(app.getHttpServer())
+    .post('/connection')
+    .send(newConnection)
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  const connectionId = JSON.parse(createdConnection.text).id;
+
+  const response = await request(app.getHttpServer())
+    .delete('/secrets/non-existent')
+    .set('Cookie', token)
+    .set('masterpwd', 'ahalaimahalai')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 404);
+});
+
+currentTest = 'GET /secrets';
+test.serial(`${currentTest} - unauthorized without token`, async (t) => {
+  const response = await request(app.getHttpServer())
+    .get('/secrets')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json');
+
+  t.is(response.status, 401);
+});
+
+currentTest = 'POST /secrets';
+test.serial(`${currentTest} - unauthorized without token`, async (t) => {
+  const createDto = {
+    slug: 'test-secret',
+    value: 'value',
+  };
+
+  const response = await request(app.getHttpServer())
+    .post('/secrets')
+    .set('Content-Type', 'application/json')
+    .set('Accept', 'application/json')
+    .send(createDto);
+
+  t.is(response.status, 401);
+});
diff --git a/docker-compose.tst.yml b/docker-compose.tst.yml
index 938d06025..0cd4c6ca9 100644
--- a/docker-compose.tst.yml
+++ b/docker-compose.tst.yml
@@ -6,9 +6,9 @@ services:
     ports:
       - 3000:3000
     env_file: ./backend/.development.env
+    environment:
+      DATABASE_URL: "postgres://postgres:abc123@postgres:5432/postgres"
     volumes:
-      - ./backend/dist:/app/dist
-      - ./backend/src:/app/src
       - ./backend/src/migrations:/app/src/migrations
     depends_on:
       - postgres
@@ -31,9 +31,9 @@ services:
     build:
       context: .
     env_file: ./backend/.development.env
+    environment:
+      DATABASE_URL: "postgres://postgres:abc123@postgres:5432/postgres"
     volumes:
-      - ./backend/dist:/app/dist
-      - ./backend/src:/app/src
       - ./backend/src/migrations:/app/src/migrations
     depends_on:
       - postgres
@@ -53,7 +53,7 @@ services:
       - test-ibm-db2-e2e-testing
       - test-mongo-e2e-testing
       - test-dynamodb-e2e-testing
-    command: ["/bin/sh", "-c", "sleep 300 && yarn test"]
+    command: ["/bin/sh", "-c", "yarn test-all test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts"]
 
   testMySQL-e2e-testing:
     image: mysql:8.0.23
@@ -235,8 +235,6 @@ services:
 
   test-dynamodb-e2e-testing:
     image: amazon/dynamodb-local
-    ports:
-      - 8000:8000
     environment:
       - AWS_ACCESS_KEY_ID=SuperSecretAwsAccessKey
       - AWS_SECRET=SuperSecretAwsSecret
diff --git a/justfile b/justfile
index de52e37d9..ca65636bc 100644
--- a/justfile
+++ b/justfile
@@ -1,2 +1,2 @@
 test:
-  docker compose  -f docker-compose.tst.yml up --abort-on-container-exit --force-recreate --build
+  docker compose  -f docker-compose.tst.yml up --abort-on-container-exit --force-recreate --build --attach=backend_test

From cf9822cee2b0bc4c72a81089d707117cd132b756 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Fri, 21 Nov 2025 22:05:23 +0000
Subject: [PATCH 02/23] secret encryption

---
 .../user-secret/user-secrets.service.ts       | 73 ++++++++++++++++---
 1 file changed, 64 insertions(+), 9 deletions(-)

diff --git a/backend/src/entities/user-secret/user-secrets.service.ts b/backend/src/entities/user-secret/user-secrets.service.ts
index 5cf7a3196..3f5d235ae 100644
--- a/backend/src/entities/user-secret/user-secrets.service.ts
+++ b/backend/src/entities/user-secret/user-secrets.service.ts
@@ -15,6 +15,7 @@ import { UpdateSecretDto } from './application/dto/update-secret.dto.js';
 import { FoundSecretDto } from './application/dto/found-secret.dto.js';
 import { SecretListResponseDto, SecretListItemDto } from './application/dto/secret-list.dto.js';
 import { AuditLogResponseDto, AuditLogEntryDto } from './application/dto/audit-log.dto.js';
+import { Encryptor } from '../../helpers/encryption/encryptor.js';
 
 @Injectable()
 export class UserSecretsService {
@@ -74,14 +75,28 @@ export class UserSecretsService {
       throw new ConflictException('Secret with this slug already exists in your company');
     }
 
+    // Encrypt the secret value
+    let encryptedValue = createDto.value;
+
+    // Layer 2: Encrypt with master password first (if enabled)
+    if (createDto.masterEncryption && createDto.masterPassword) {
+      encryptedValue = Encryptor.encryptDataMasterPwd(encryptedValue, createDto.masterPassword);
+    }
+
+    // Layer 1: Encrypt with PRIVATE_KEY (always)
+    encryptedValue = Encryptor.encryptData(encryptedValue);
+
+    // Hash master password if provided
+    const masterHash = createDto.masterPassword ? await Encryptor.hashUserPassword(createDto.masterPassword) : null;
+
     // Create secret
     const secret = this.secretRepository.create({
       slug: createDto.slug,
-      encryptedValue: createDto.value, // TODO: Encrypt with PRIVATE_KEY and optionally master password
+      encryptedValue,
       companyId: user.company.id,
       expiresAt: createDto.expiresAt ? new Date(createDto.expiresAt) : null,
       masterEncryption: createDto.masterEncryption || false,
-      masterHash: createDto.masterPassword ? 'TODO: Hash master password' : null, // TODO: Hash master password
+      masterHash,
     });
 
     const saved = await this.secretRepository.save(secret);
@@ -151,8 +166,8 @@ export class UserSecretsService {
     }
 
     if (secret.masterEncryption && masterPassword) {
-      // TODO: Verify master password
-      const isValid = true; // TODO: Verify against masterHash
+      // Verify master password against stored hash
+      const isValid = await Encryptor.verifyUserPassword(masterPassword, secret.masterHash);
       if (!isValid) {
         await this.logAccess(secret.id, userId, SecretActionEnum.VIEW, false, 'Invalid master password');
         throw new ForbiddenException('Invalid master password');
@@ -166,7 +181,7 @@ export class UserSecretsService {
     // Log access
     await this.logAccess(secret.id, userId, SecretActionEnum.VIEW);
 
-    return this.buildFoundSecretDto(secret, true);
+    return this.buildFoundSecretDto(secret, true, masterPassword);
   }
 
   async updateSecret(
@@ -198,8 +213,30 @@ export class UserSecretsService {
       throw new ForbiddenException('Master password required');
     }
 
-    // Update secret
-    secret.encryptedValue = updateDto.value; // TODO: Encrypt
+    // Verify master password if required
+    if (secret.masterEncryption && masterPassword) {
+      const isValid = await Encryptor.verifyUserPassword(masterPassword, secret.masterHash);
+      if (!isValid) {
+        await this.logAccess(secret.id, userId, SecretActionEnum.UPDATE, false, 'Invalid master password');
+        throw new ForbiddenException('Invalid master password');
+      }
+    }
+
+    // Encrypt the updated secret value if provided
+    if (updateDto.value) {
+      let encryptedValue = updateDto.value;
+
+      // Layer 2: Encrypt with master password first (if enabled)
+      if (secret.masterEncryption && masterPassword) {
+        encryptedValue = Encryptor.encryptDataMasterPwd(encryptedValue, masterPassword);
+      }
+
+      // Layer 1: Encrypt with PRIVATE_KEY (always)
+      encryptedValue = Encryptor.encryptData(encryptedValue);
+
+      secret.encryptedValue = encryptedValue;
+    }
+
     if (updateDto.expiresAt !== undefined) {
       secret.expiresAt = updateDto.expiresAt ? new Date(updateDto.expiresAt) : null;
     }
@@ -279,11 +316,29 @@ export class UserSecretsService {
     };
   }
 
-  private buildFoundSecretDto(secret: UserSecretEntity, includeValue: boolean): FoundSecretDto {
+  private buildFoundSecretDto(
+    secret: UserSecretEntity,
+    includeValue: boolean,
+    masterPassword?: string,
+  ): FoundSecretDto {
+    let decryptedValue: string | undefined = undefined;
+
+    if (includeValue) {
+      // Layer 1: Decrypt with PRIVATE_KEY (always)
+      let value = Encryptor.decryptData(secret.encryptedValue);
+
+      // Layer 2: Decrypt with master password (if enabled)
+      if (secret.masterEncryption && masterPassword) {
+        value = Encryptor.decryptDataMasterPwd(value, masterPassword);
+      }
+
+      decryptedValue = value;
+    }
+
     return {
       id: secret.id,
       slug: secret.slug,
-      value: includeValue ? secret.encryptedValue : undefined, // TODO: Decrypt
+      value: decryptedValue,
       companyId: secret.companyId,
       createdAt: secret.createdAt,
       updatedAt: secret.updatedAt,

From 8ec15931301b2ac84f235bd1144b83884799435b Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Fri, 28 Nov 2025 12:00:11 +0000
Subject: [PATCH 03/23] remove unused decorator, fix migration and tests

---
 AGENTS.md                                     |  1 +
 Dockerfile                                    |  6 +--
 backend/ava.config.mjs                        |  2 +-
 .../src/decorators/company-id.decorator.ts    | 23 ---------
 .../user-secret/user-secret.controller.ts     |  1 -
 .../user-secret/user-secret.entity.ts         |  2 +-
 .../user-secret/user-secret.module.ts         | 37 ++++++++++++--
 .../1763724061000-CreateUserSecretEntity.ts   |  2 +-
 .../non-saas-secrets-e2e.test.ts              | 48 +++++++++----------
 docker-compose.tst.yml                        |  9 +++-
 justfile                                      |  4 +-
 rocketadmin-agent/tsconfig.build.json         |  2 +-
 12 files changed, 75 insertions(+), 62 deletions(-)
 create mode 120000 AGENTS.md
 delete mode 100644 backend/src/decorators/company-id.decorator.ts

diff --git a/AGENTS.md b/AGENTS.md
new file mode 120000
index 000000000..681311eb9
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1 @@
+CLAUDE.md
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index b7e3186c8..c7d02c01d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,7 +8,7 @@ COPY frontend/.yarn /app/frontend/.yarn
 RUN apt-get update && apt-get install -y \
     git \
     && rm -rf /var/lib/apt/lists/*
-RUN yarn install --immutable --network-timeout 1000000 --silent
+RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --immutable --network-timeout 1000000 --silent
 COPY frontend/scripts /app/frontend/scripts
 COPY frontend/src /app/frontend/src
 
@@ -40,9 +40,9 @@ COPY backend /app/backend
 COPY shared-code /app/shared-code
 COPY rocketadmin-agent /app/rocketadmin-agent
 COPY .yarn /app/.yarn
-RUN yarn install --network-timeout 1000000 --immutable --silent
+RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --network-timeout 1000000 --immutable --silent
 RUN cd shared-code && ../node_modules/.bin/tsc
-RUN cd backend && yarn run nest build
+RUN cd backend && ../node_modules/.bin/tsc && yarn run nest build
 COPY --from=front_builder /app/frontend/dist/dissendium-v0 /var/www/html
 COPY frontend/nginx/default.conf /etc/nginx/sites-enabled/default
 RUN mkdir -p /app/backend/node_modules/.cache && chown -R appuser:appuser /app/backend/node_modules/.cache
diff --git a/backend/ava.config.mjs b/backend/ava.config.mjs
index e36a982fa..ed404e499 100644
--- a/backend/ava.config.mjs
+++ b/backend/ava.config.mjs
@@ -8,7 +8,7 @@ export default {
       'src/': 'dist/src/',
       'test/': 'dist/test/',
     },
-    compile: 'tsc',
+    compile: false,
   },
   workerThreads: false,
   verbose: true,
diff --git a/backend/src/decorators/company-id.decorator.ts b/backend/src/decorators/company-id.decorator.ts
deleted file mode 100644
index 8f0b01e0c..000000000
--- a/backend/src/decorators/company-id.decorator.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common';
-import { IRequestWithCognitoInfo } from '../authorization/index.js';
-import { Messages } from '../exceptions/text/messages.js';
-
-export const CompanyId = createParamDecorator(async (data: any, ctx: ExecutionContext): Promise => {
-  const request: IRequestWithCognitoInfo = ctx.switchToHttp().getRequest();
-  const userId = request.decoded?.sub;
-
-  if (!userId) {
-    throw new BadRequestException(Messages.USER_ID_MISSING);
-  }
-
-  // Company ID should be retrieved from the user entity via a repository lookup
-  // This is a simplified version - in practice, you'd inject a UserRepository
-  // For now, we'll assume the company ID is added to the request by middleware
-  const companyId = (request as any).companyId;
-
-  if (!companyId) {
-    throw new BadRequestException('Company ID not found for user');
-  }
-
-  return companyId;
-});
diff --git a/backend/src/entities/user-secret/user-secret.controller.ts b/backend/src/entities/user-secret/user-secret.controller.ts
index e60d17bf6..bf8c9cfa2 100644
--- a/backend/src/entities/user-secret/user-secret.controller.ts
+++ b/backend/src/entities/user-secret/user-secret.controller.ts
@@ -10,7 +10,6 @@ import {
   Post,
   Put,
   Query,
-  UseGuards,
   UseInterceptors,
 } from '@nestjs/common';
 import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
diff --git a/backend/src/entities/user-secret/user-secret.entity.ts b/backend/src/entities/user-secret/user-secret.entity.ts
index 1b6ae3299..55ca935ff 100644
--- a/backend/src/entities/user-secret/user-secret.entity.ts
+++ b/backend/src/entities/user-secret/user-secret.entity.ts
@@ -48,7 +48,7 @@ export class UserSecretEntity {
   @Column({ default: false })
   masterEncryption: boolean;
 
-  @Column({ type: 'varchar', length: 255, nullable: true })
+  @Column({ type: 'varchar', length: 4096, nullable: true })
   masterHash: string;
 
   @OneToMany(() => SecretAccessLogEntity, (log) => log.secret)
diff --git a/backend/src/entities/user-secret/user-secret.module.ts b/backend/src/entities/user-secret/user-secret.module.ts
index d659dbc5d..173ae03d9 100644
--- a/backend/src/entities/user-secret/user-secret.module.ts
+++ b/backend/src/entities/user-secret/user-secret.module.ts
@@ -1,15 +1,46 @@
-import { Module } from '@nestjs/common';
+import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { UserSecretEntity } from './user-secret.entity.js';
 import { SecretAccessLogEntity } from '../secret-access-log/secret-access-log.entity.js';
 import { UserEntity } from '../user/user.entity.js';
 import { UserSecretController } from './user-secret.controller.js';
 import { UserSecretsService } from './user-secrets.service.js';
+import { AuthMiddleware } from '../../authorization/auth.middleware.js';
+import { LogOutEntity } from '../log-out/log-out.entity.js';
 
 @Module({
-  imports: [TypeOrmModule.forFeature([UserSecretEntity, SecretAccessLogEntity, UserEntity])],
+  imports: [TypeOrmModule.forFeature([UserSecretEntity, SecretAccessLogEntity, UserEntity, LogOutEntity])],
   providers: [UserSecretsService],
   controllers: [UserSecretController],
   exports: [UserSecretsService],
 })
-export class UserSecretModule {}
+export class UserSecretModule implements NestModule {
+  public configure(consumer: MiddlewareConsumer): void {
+    consumer.apply(AuthMiddleware).forRoutes(
+      {
+        path: '/secrets',
+        method: RequestMethod.POST,
+      },
+      {
+        path: '/secrets',
+        method: RequestMethod.GET,
+      },
+      {
+        path: '/secrets/:slug',
+        method: RequestMethod.GET,
+      },
+      {
+        path: '/secrets/:slug',
+        method: RequestMethod.PUT,
+      },
+      {
+        path: '/secrets/:slug',
+        method: RequestMethod.DELETE,
+      },
+      {
+        path: '/secrets/:slug/audit-log',
+        method: RequestMethod.GET,
+      },
+    );
+  }
+}
diff --git a/backend/src/migrations/1763724061000-CreateUserSecretEntity.ts b/backend/src/migrations/1763724061000-CreateUserSecretEntity.ts
index 2c399b1cb..805608c81 100644
--- a/backend/src/migrations/1763724061000-CreateUserSecretEntity.ts
+++ b/backend/src/migrations/1763724061000-CreateUserSecretEntity.ts
@@ -14,7 +14,7 @@ export class CreateUserSecretEntity1763724061000 implements MigrationInterface {
             "lastAccessedAt" TIMESTAMP,
             "expiresAt" TIMESTAMP,
             "masterEncryption" boolean NOT NULL DEFAULT false,
-            "masterHash" character varying(255),
+            "masterHash" character varying(4096),
             CONSTRAINT "PK_user_secrets" PRIMARY KEY ("id")
         )`);
 
diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
index f271a6991..64e4c9ea9 100644
--- a/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
+++ b/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
@@ -1,5 +1,4 @@
 /* eslint-disable @typescript-eslint/no-unused-vars */
-import { faker } from '@faker-js/faker';
 import { INestApplication, ValidationPipe } from '@nestjs/common';
 import { Test } from '@nestjs/testing';
 import test from 'ava';
@@ -7,7 +6,6 @@ import cookieParser from 'cookie-parser';
 import request from 'supertest';
 import { ApplicationModule } from '../../../src/app.module.js';
 import { AllExceptionsFilter } from '../../../src/exceptions/all-exceptions.filter.js';
-import { Messages } from '../../../src/exceptions/text/messages.js';
 import { Cacher } from '../../../src/helpers/cache/cacher.js';
 import { DatabaseModule } from '../../../src/shared/database/database.module.js';
 import { DatabaseService } from '../../../src/shared/database/database.service.js';
@@ -84,7 +82,7 @@ test.serial(`${currentTest} - should create a new secret`, async (t) => {
     .set('Accept', 'application/json')
     .send(createDto);
 
-  t.is(response.status, 201);
+  t.is(response.status, 201, response.text);
   const responseBody = JSON.parse(response.text);
   t.is(responseBody.slug, 'test-api-key');
   t.is(responseBody.masterEncryption, false);
@@ -119,7 +117,7 @@ test.serial(`${currentTest} - should return 409 for duplicate slug`, async (t) =
     .set('Accept', 'application/json')
     .send(createDto);
 
-  t.is(response.status, 409);
+  t.is(response.status, 409, response.text);
   const responseBody = JSON.parse(response.text);
   t.truthy(responseBody.message);
 });
@@ -150,7 +148,7 @@ test.serial(`${currentTest} - should validate slug format`, async (t) => {
     .set('Accept', 'application/json')
     .send(createDto);
 
-  t.is(response.status, 400);
+  t.is(response.status, 400, response.text);
   const responseBody = JSON.parse(response.text);
   t.truthy(responseBody.message);
 });
@@ -175,7 +173,7 @@ test.serial(`${currentTest} - should return list of company secrets`, async (t)
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 200);
+  t.is(response.status, 200, response.text);
   const responseBody = JSON.parse(response.text);
   t.truthy(responseBody.data);
   t.true(Array.isArray(responseBody.data));
@@ -207,7 +205,7 @@ test.serial(`${currentTest}?search=test - should filter secrets by slug`, async
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 200);
+  t.is(response.status, 200, response.text);
   const responseBody = JSON.parse(response.text);
   t.truthy(responseBody.data);
   t.true(responseBody.data.every((s: any) => s.slug.includes('test')));
@@ -233,7 +231,7 @@ test.serial(`${currentTest} - should return secret with value`, async (t) => {
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 200);
+  t.is(response.status, 200, response.text);
   const responseBody = JSON.parse(response.text);
   t.is(responseBody.slug, 'test-api-key');
   t.truthy(responseBody.value);
@@ -259,7 +257,7 @@ test.serial(`${currentTest} - should return 404 for non-existent secret`, async
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 404);
+  t.is(response.status, 404, response.text);
 });
 
 currentTest = 'POST /secrets';
@@ -291,7 +289,7 @@ test.serial(`${currentTest} - should create secret with master password`, async
     .set('Accept', 'application/json')
     .send(createDto);
 
-  t.is(response.status, 201);
+  t.is(response.status, 201, response.text);
   const responseBody = JSON.parse(response.text);
   t.is(responseBody.slug, 'protected-secret');
   t.is(responseBody.masterEncryption, true);
@@ -317,7 +315,7 @@ test.serial(`${currentTest} - should require master password for protected secre
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 403);
+  t.is(response.status, 403, response.text);
 });
 
 test.serial(`${currentTest} - should return protected secret with correct master password`, async (t) => {
@@ -340,7 +338,7 @@ test.serial(`${currentTest} - should return protected secret with correct master
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 200);
+  t.is(response.status, 200, response.text);
   const responseBody = JSON.parse(response.text);
   t.is(responseBody.slug, 'protected-secret');
   t.truthy(responseBody.value);
@@ -372,7 +370,7 @@ test.serial(`${currentTest} - should update secret value`, async (t) => {
     .set('Accept', 'application/json')
     .send(updateDto);
 
-  t.is(response.status, 200);
+  t.is(response.status, 200, response.text);
   const responseBody = JSON.parse(response.text);
   t.is(responseBody.slug, 'test-api-key');
   t.truthy(responseBody.updatedAt);
@@ -407,7 +405,7 @@ test.serial(`${currentTest} - should update expiration date`, async (t) => {
     .set('Accept', 'application/json')
     .send(updateDto);
 
-  t.is(response.status, 200);
+  t.is(response.status, 200, response.text);
   const responseBody = JSON.parse(response.text);
   t.truthy(responseBody.expiresAt);
 });
@@ -437,7 +435,7 @@ test.serial(`${currentTest} - should return 404 for non-existent secret`, async
     .set('Accept', 'application/json')
     .send(updateDto);
 
-  t.is(response.status, 404);
+  t.is(response.status, 404, response.text);
 });
 
 currentTest = 'GET /secrets/:slug/audit-log';
@@ -460,7 +458,7 @@ test.serial(`${currentTest} - should return audit log entries`, async (t) => {
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 200);
+  t.is(response.status, 200, response.text);
   const responseBody = JSON.parse(response.text);
   t.truthy(responseBody.data);
   t.true(Array.isArray(responseBody.data));
@@ -495,9 +493,9 @@ test.serial(`${currentTest}?page=1&limit=2 - should paginate audit log`, async (
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 200);
+  t.is(response.status, 200, response.text);
   const responseBody = JSON.parse(response.text);
-  t.is(responseBody.pagination.limit, 2);
+  t.is(responseBody.pagination.limit, '2');
   t.true(responseBody.data.length <= 2);
 });
 
@@ -532,7 +530,7 @@ test.serial(`${currentTest} - should create expired secret`, async (t) => {
     .set('Accept', 'application/json')
     .send(createDto);
 
-  t.is(response.status, 201);
+  t.is(response.status, 201, response.text);
 });
 
 currentTest = 'GET /secrets/:slug';
@@ -555,7 +553,7 @@ test.serial(`${currentTest} - should return 410 for expired secret`, async (t) =
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 410);
+  t.is(response.status, 410, response.text);
 });
 
 currentTest = 'DELETE /secrets/:slug';
@@ -579,7 +577,7 @@ test.serial(`${currentTest} - should delete secret`, async (t) => {
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 200);
+  t.is(response.status, 200, response.text);
   const responseBody = JSON.parse(response.text);
   t.is(responseBody.message, 'Secret deleted successfully');
   t.truthy(responseBody.deletedAt);
@@ -605,7 +603,7 @@ test.serial(`${currentTest} - should return 404 after deletion`, async (t) => {
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 404);
+  t.is(response.status, 404, response.text);
 });
 
 currentTest = 'DELETE /secrets/:slug';
@@ -629,7 +627,7 @@ test.serial(`${currentTest} - should return 404 for non-existent secret`, async
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 404);
+  t.is(response.status, 404, response.text);
 });
 
 currentTest = 'GET /secrets';
@@ -639,7 +637,7 @@ test.serial(`${currentTest} - unauthorized without token`, async (t) => {
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  t.is(response.status, 401);
+  t.is(response.status, 401, response.text);
 });
 
 currentTest = 'POST /secrets';
@@ -655,5 +653,5 @@ test.serial(`${currentTest} - unauthorized without token`, async (t) => {
     .set('Accept', 'application/json')
     .send(createDto);
 
-  t.is(response.status, 401);
+  t.is(response.status, 401, response.text);
 });
diff --git a/docker-compose.tst.yml b/docker-compose.tst.yml
index 0cd4c6ca9..87e78c4ec 100644
--- a/docker-compose.tst.yml
+++ b/docker-compose.tst.yml
@@ -33,6 +33,7 @@ services:
     env_file: ./backend/.development.env
     environment:
       DATABASE_URL: "postgres://postgres:abc123@postgres:5432/postgres"
+      EXTRA_ARGS: "${EXTRA_ARGS:-}"
     volumes:
       - ./backend/src/migrations:/app/src/migrations
     depends_on:
@@ -53,7 +54,11 @@ services:
       - test-ibm-db2-e2e-testing
       - test-mongo-e2e-testing
       - test-dynamodb-e2e-testing
-    command: ["/bin/sh", "-c", "yarn test-all test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts"]
+    command: ["/bin/sh", "-c", "yarn test-all $EXTRA_ARGS"]
+    develop:
+      watch:
+        - action: rebuild
+          path: ./backend/src
 
   testMySQL-e2e-testing:
     image: mysql:8.0.23
@@ -89,6 +94,8 @@ services:
     environment:
       POSTGRES_PASSWORD: abc123
     command: postgres -c 'max_connections=300'
+    tmpfs:
+      - /var/lib/postgresql
 
   mssql-e2e-testing:
     image: mcr.microsoft.com/mssql/server:2019-latest
diff --git a/justfile b/justfile
index ca65636bc..507382c7e 100644
--- a/justfile
+++ b/justfile
@@ -1,2 +1,2 @@
-test:
-  docker compose  -f docker-compose.tst.yml up --abort-on-container-exit --force-recreate --build --attach=backend_test
+test args='':
+  EXTRA_ARGS="{{args}}" docker compose  -f docker-compose.tst.yml up --abort-on-container-exit --force-recreate --build --attach=backend_test --no-log-prefix
diff --git a/rocketadmin-agent/tsconfig.build.json b/rocketadmin-agent/tsconfig.build.json
index 64f86c6bd..79548c6dc 100644
--- a/rocketadmin-agent/tsconfig.build.json
+++ b/rocketadmin-agent/tsconfig.build.json
@@ -1,4 +1,4 @@
 {
   "extends": "./tsconfig.json",
-  "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
+  "exclude": ["node_modules", "dist", "**/*spec.ts"]
 }

From 27c38096f57a77d297c9d2ed77703c5fe4486d3c Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Fri, 28 Nov 2025 12:39:17 +0000
Subject: [PATCH 04/23] improve architecture

---
 CLAUDE.md                                     | 128 +++++-
 .../global-database-context.interface.ts      |   6 +
 .../application/global-database-context.ts    |  22 +
 backend/src/common/data-injection.tokens.ts   |   7 +
 .../secret-access-log-repository.extension.ts |  41 ++
 .../secret-access-log-repository.interface.ts |  15 +
 .../secret-access-log.entity.ts               |   4 +-
 .../data-structures/create-secret.ds.ts       |   8 +
 .../data-structures/created-secret.ds.ts      |  10 +
 .../data-structures/delete-secret.ds.ts       |   9 +
 .../data-structures/found-secret.ds.ts        |  11 +
 .../data-structures/get-audit-log.ds.ts       |  32 ++
 .../data-structures/get-secret.ds.ts          |   5 +
 .../data-structures/get-secrets.ds.ts         |  27 ++
 .../data-structures/update-secret.ds.ts       |  18 +
 .../application/dto/create-secret.dto.ts      |   1 -
 .../user-secret-repository.extension.ts       |  38 ++
 .../user-secret-repository.interface.ts       |   9 +
 .../use-cases/create-secret.use.case.ts       |  64 +++
 .../use-cases/delete-secret.use.case.ts       |  45 +++
 .../get-secret-audit-log.use.case.ts          |  54 +++
 .../use-cases/get-secret-by-slug.use.case.ts  |  74 ++++
 .../use-cases/get-secrets.use.case.ts         |  46 +++
 .../use-cases/update-secret.use.case.ts       |  81 ++++
 .../user-secret-use-cases.interface.ts        |  33 ++
 .../user-secret/user-secret.controller.ts     |  81 +++-
 .../user-secret/user-secret.entity.ts         |   2 +-
 .../user-secret/user-secret.module.ts         |  41 +-
 .../user-secret/user-secrets.service.spec.ts  | 280 -------------
 .../user-secret/user-secrets.service.ts       | 379 ------------------
 .../utils/build-audit-log-entry.ds.ts         |  18 +
 .../user-secret/utils/build-audit-log.dto.ts  |  22 +
 .../utils/build-created-secret.ds.ts          |  15 +
 .../utils/build-created-secret.dto.ts         |  16 +
 .../utils/build-found-secret.ds.ts            |  16 +
 .../utils/build-found-secret.dto.ts           |  16 +
 .../utils/build-secret-list-item.ds.ts        |  15 +
 .../utils/build-secret-list.dto.ts            |  22 +
 .../utils/build-updated-secret.ds.ts          |  15 +
 .../utils/build-updated-secret.dto.ts         |  16 +
 40 files changed, 1060 insertions(+), 682 deletions(-)
 create mode 100644 backend/src/entities/secret-access-log/repository/secret-access-log-repository.extension.ts
 create mode 100644 backend/src/entities/secret-access-log/repository/secret-access-log-repository.interface.ts
 create mode 100644 backend/src/entities/user-secret/application/data-structures/create-secret.ds.ts
 create mode 100644 backend/src/entities/user-secret/application/data-structures/created-secret.ds.ts
 create mode 100644 backend/src/entities/user-secret/application/data-structures/delete-secret.ds.ts
 create mode 100644 backend/src/entities/user-secret/application/data-structures/found-secret.ds.ts
 create mode 100644 backend/src/entities/user-secret/application/data-structures/get-audit-log.ds.ts
 create mode 100644 backend/src/entities/user-secret/application/data-structures/get-secret.ds.ts
 create mode 100644 backend/src/entities/user-secret/application/data-structures/get-secrets.ds.ts
 create mode 100644 backend/src/entities/user-secret/application/data-structures/update-secret.ds.ts
 create mode 100644 backend/src/entities/user-secret/repository/user-secret-repository.extension.ts
 create mode 100644 backend/src/entities/user-secret/repository/user-secret-repository.interface.ts
 create mode 100644 backend/src/entities/user-secret/use-cases/create-secret.use.case.ts
 create mode 100644 backend/src/entities/user-secret/use-cases/delete-secret.use.case.ts
 create mode 100644 backend/src/entities/user-secret/use-cases/get-secret-audit-log.use.case.ts
 create mode 100644 backend/src/entities/user-secret/use-cases/get-secret-by-slug.use.case.ts
 create mode 100644 backend/src/entities/user-secret/use-cases/get-secrets.use.case.ts
 create mode 100644 backend/src/entities/user-secret/use-cases/update-secret.use.case.ts
 create mode 100644 backend/src/entities/user-secret/use-cases/user-secret-use-cases.interface.ts
 delete mode 100644 backend/src/entities/user-secret/user-secrets.service.spec.ts
 delete mode 100644 backend/src/entities/user-secret/user-secrets.service.ts
 create mode 100644 backend/src/entities/user-secret/utils/build-audit-log-entry.ds.ts
 create mode 100644 backend/src/entities/user-secret/utils/build-audit-log.dto.ts
 create mode 100644 backend/src/entities/user-secret/utils/build-created-secret.ds.ts
 create mode 100644 backend/src/entities/user-secret/utils/build-created-secret.dto.ts
 create mode 100644 backend/src/entities/user-secret/utils/build-found-secret.ds.ts
 create mode 100644 backend/src/entities/user-secret/utils/build-found-secret.dto.ts
 create mode 100644 backend/src/entities/user-secret/utils/build-secret-list-item.ds.ts
 create mode 100644 backend/src/entities/user-secret/utils/build-secret-list.dto.ts
 create mode 100644 backend/src/entities/user-secret/utils/build-updated-secret.ds.ts
 create mode 100644 backend/src/entities/user-secret/utils/build-updated-secret.dto.ts

diff --git a/CLAUDE.md b/CLAUDE.md
index 23b8ed94f..27e034ffb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,8 +1,126 @@
-# Claude Code Configuration
+# CLAUDE.md
 
-## Frontend Tests
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+RocketAdmin is a database administration panel that allows users to manage database connections, tables, and data. It consists of multiple components in a monorepo structure:
+
+- **backend/** - NestJS API server (TypeScript, ES modules)
+- **frontend/** - Angular 19 web application (standalone components)
+- **rocketadmin-agent/** - NestJS agent for connecting to databases behind firewalls
+- **autoadmin-ws-server/** - WebSocket server for agent communication
+- **shared-code/** - Shared data access layer and utilities used by backend and agent
+
+## Development Commands
+
+### Backend
 
-To run frontend tests:
 ```bash
-cd frontend && yarn test --browsers=ChromeHeadlessCustom --no-watch --no-progress
-```
\ No newline at end of file
+cd backend
+yarn start:dev          # Start dev server with hot reload
+yarn build              # Build for production
+yarn lint               # ESLint with auto-fix
+yarn test               # Run non-saas AVA tests (serial)
+yarn test-all           # Run all AVA tests (5min timeout, serial)
+yarn test-saas          # Run SaaS-specific tests
+```
+
+### Frontend
+
+```bash
+cd frontend
+yarn start              # Start Angular dev server
+yarn build              # Production build
+yarn test:ci            # Run tests headlessly (CI mode)
+yarn test --browsers=ChromeHeadlessCustom --no-watch --no-progress  # Headless tests
+yarn lint               # TSLint (deprecated, needs ESLint migration)
+```
+
+### Running Backend Tests with Docker
+
+The project uses `just` for test orchestration:
+
+```bash
+just test               # Run all backend tests with Docker Compose
+just test "path/to/test.ts"  # Run specific test file
+```
+
+This spins up test databases (MySQL, PostgreSQL, MSSQL, Oracle, IBM DB2, MongoDB, DynamoDB) via `docker-compose.tst.yml`.
+
+### Migrations
+
+```bash
+cd backend
+yarn build              # Must build first
+yarn migration:generate src/migrations/MigrationName  # Generate migration
+yarn migration:run      # Run pending migrations
+yarn migration:revert   # Revert last migration
+```
+
+## Architecture
+
+### Monorepo Structure
+
+- Uses Yarn workspaces with packages: `backend`, `rocketadmin-agent`, `shared-code`
+- `shared-code` is imported as `@rocketadmin/shared-code` workspace dependency
+- Frontend is a separate Angular project (not a workspace member)
+
+### Backend (NestJS)
+
+- **Entities pattern**: Each entity has its own directory under `src/entities/` containing:
+  - `*.entity.ts` - TypeORM entity
+  - `*.module.ts` - NestJS module
+  - `*.controller.ts` - REST endpoints
+  - `*.service.ts` - Business logic (use cases)
+  - `dto/` - Request/response DTOs with class-validator decorators
+  - `*.controller.ee.ts` - Enterprise edition controllers (SaaS features)
+- **Guards**: Authentication and authorization in `src/guards/`
+- **Data access**: Uses `shared-code` for database operations via Knex
+- **Testing**: AVA test framework with tests in `test/ava-tests/`
+  - `non-saas-tests/` - Core functionality tests
+  - `saas-tests/` - SaaS-specific feature tests
+  - `complex-table-tests/` - Complex table operation tests
+
+### Frontend (Angular 19)
+
+See `frontend/CLAUDE.md` for detailed frontend architecture.
+
+Key points:
+- Standalone components (no NgModules)
+- BehaviorSubject-based state management (no NgRx)
+- Multi-environment builds (development, production, saas, saas-production)
+- Jasmine/Karma testing with ChromeHeadless
+
+### Shared Code
+
+Located in `shared-code/src/`:
+- `data-access-layer/` - Database abstraction supporting MySQL, PostgreSQL, MSSQL, Oracle, MongoDB, DynamoDB, IBM DB2, Cassandra, Elasticsearch
+- `knex-manager/` - Knex connection management
+- `caching/` - LRU cache utilities
+- `helpers/` - Shared utilities
+
+### Agent Architecture
+
+The rocketadmin-agent connects to databases in private networks:
+1. Agent runs inside customer's network
+2. Connects to `autoadmin-ws-server` via WebSocket
+3. Backend communicates with agent through WebSocket server
+4. Agent executes database queries and returns results
+
+## Database Support
+
+The application supports: MySQL, PostgreSQL, MongoDB, DynamoDB, Cassandra, OracleDB, MSSQL, IBM DB2, Elasticsearch, Redis
+
+Database-specific DAOs are in `shared-code/src/data-access-layer/`.
+
+## Testing Database Connections
+
+Test databases are defined in `docker-compose.tst.yml`:
+- MySQL: `testMySQL-e2e-testing:3306`
+- PostgreSQL: `testPg-e2e-testing:5432`
+- MSSQL: `mssql-e2e-testing:1433`
+- Oracle: `test-oracle-e2e-testing:1521`
+- IBM DB2: `test-ibm-db2-e2e-testing:50000`
+- MongoDB: `test-mongo-e2e-testing:27017`
+- DynamoDB: `test-dynamodb-e2e-testing:8000`
diff --git a/backend/src/common/application/global-database-context.interface.ts b/backend/src/common/application/global-database-context.interface.ts
index 08245f31a..62bf04541 100644
--- a/backend/src/common/application/global-database-context.interface.ts
+++ b/backend/src/common/application/global-database-context.interface.ts
@@ -48,6 +48,10 @@ import { IDatabaseContext } from '../database-context.interface.js';
 import { TableCategoriesEntity } from '../../entities/table-categories/table-categories.entity.js';
 import { ITableCategoriesCustomRepository } from '../../entities/table-categories/repository/table-categories-repository.interface.js';
 import { ConnectionPropertiesEntity } from '../../entities/connection-properties/connection-properties.entity.js';
+import { UserSecretEntity } from '../../entities/user-secret/user-secret.entity.js';
+import { IUserSecretRepository } from '../../entities/user-secret/repository/user-secret-repository.interface.js';
+import { SecretAccessLogEntity } from '../../entities/secret-access-log/secret-access-log.entity.js';
+import { ISecretAccessLogRepository } from '../../entities/secret-access-log/repository/secret-access-log-repository.interface.js';
 
 export interface IGlobalDatabaseContext extends IDatabaseContext {
   userRepository: Repository & IUserRepository;
@@ -83,4 +87,6 @@ export interface IGlobalDatabaseContext extends IDatabaseContext {
   tableFiltersRepository: Repository & ITableFiltersCustomRepository;
   aiResponsesToUserRepository: Repository & IAiResponsesToUserRepository;
   tableCategoriesRepository: Repository & ITableCategoriesCustomRepository;
+  userSecretRepository: Repository & IUserSecretRepository;
+  secretAccessLogRepository: Repository & ISecretAccessLogRepository;
 }
diff --git a/backend/src/common/application/global-database-context.ts b/backend/src/common/application/global-database-context.ts
index d8e229214..529794ed9 100644
--- a/backend/src/common/application/global-database-context.ts
+++ b/backend/src/common/application/global-database-context.ts
@@ -90,6 +90,12 @@ import { aiResponsesToUserRepositoryExtension } from '../../entities/ai/ai-data-
 import { TableCategoriesEntity } from '../../entities/table-categories/table-categories.entity.js';
 import { ITableCategoriesCustomRepository } from '../../entities/table-categories/repository/table-categories-repository.interface.js';
 import { tableCategoriesCustomRepositoryExtension } from '../../entities/table-categories/repository/table-categories-repository.extension.js';
+import { UserSecretEntity } from '../../entities/user-secret/user-secret.entity.js';
+import { IUserSecretRepository } from '../../entities/user-secret/repository/user-secret-repository.interface.js';
+import { userSecretRepositoryExtension } from '../../entities/user-secret/repository/user-secret-repository.extension.js';
+import { SecretAccessLogEntity } from '../../entities/secret-access-log/secret-access-log.entity.js';
+import { ISecretAccessLogRepository } from '../../entities/secret-access-log/repository/secret-access-log-repository.interface.js';
+import { secretAccessLogRepositoryExtension } from '../../entities/secret-access-log/repository/secret-access-log-repository.extension.js';
 
 @Injectable({ scope: Scope.REQUEST })
 export class GlobalDatabaseContext implements IGlobalDatabaseContext {
@@ -128,6 +134,8 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext {
   private _tableFiltersRepository: Repository & ITableFiltersCustomRepository;
   private _aiResponsesToUserRepository: Repository & IAiResponsesToUserRepository;
   private _tableCategoriesRepository: Repository & ITableCategoriesCustomRepository;
+  private _userSecretRepository: Repository & IUserSecretRepository;
+  private _secretAccessLogRepository: Repository & ISecretAccessLogRepository;
 
   public constructor(
     @Inject(BaseType.DATA_SOURCE)
@@ -216,6 +224,12 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext {
     this._tableCategoriesRepository = this.appDataSource
       .getRepository(TableCategoriesEntity)
       .extend(tableCategoriesCustomRepositoryExtension);
+    this._userSecretRepository = this.appDataSource
+      .getRepository(UserSecretEntity)
+      .extend(userSecretRepositoryExtension);
+    this._secretAccessLogRepository = this.appDataSource
+      .getRepository(SecretAccessLogEntity)
+      .extend(secretAccessLogRepositoryExtension);
   }
 
   public get userRepository(): Repository & IUserRepository {
@@ -350,6 +364,14 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext {
     return this._tableCategoriesRepository;
   }
 
+  public get userSecretRepository(): Repository & IUserSecretRepository {
+    return this._userSecretRepository;
+  }
+
+  public get secretAccessLogRepository(): Repository & ISecretAccessLogRepository {
+    return this._secretAccessLogRepository;
+  }
+
   public startTransaction(): Promise {
     this._queryRunner = this.appDataSource.createQueryRunner();
     this._queryRunner.startTransaction();
diff --git a/backend/src/common/data-injection.tokens.ts b/backend/src/common/data-injection.tokens.ts
index 241b4d9f1..3de93ed0e 100644
--- a/backend/src/common/data-injection.tokens.ts
+++ b/backend/src/common/data-injection.tokens.ts
@@ -171,4 +171,11 @@ export enum UseCaseType {
 
   CREATE_UPDATE_TABLE_CATEGORIES = 'CREATE_UPDATE_TABLE_CATEGORIES',
   FIND_TABLE_CATEGORIES = 'FIND_TABLE_CATEGORIES',
+
+  CREATE_SECRET = 'CREATE_SECRET',
+  GET_SECRETS = 'GET_SECRETS',
+  GET_SECRET_BY_SLUG = 'GET_SECRET_BY_SLUG',
+  UPDATE_SECRET = 'UPDATE_SECRET',
+  DELETE_SECRET = 'DELETE_SECRET',
+  GET_SECRET_AUDIT_LOG = 'GET_SECRET_AUDIT_LOG',
 }
diff --git a/backend/src/entities/secret-access-log/repository/secret-access-log-repository.extension.ts b/backend/src/entities/secret-access-log/repository/secret-access-log-repository.extension.ts
new file mode 100644
index 000000000..5ed5f6cb7
--- /dev/null
+++ b/backend/src/entities/secret-access-log/repository/secret-access-log-repository.extension.ts
@@ -0,0 +1,41 @@
+import { SecretAccessLogEntity, SecretActionEnum } from '../secret-access-log.entity.js';
+import { ISecretAccessLogRepository } from './secret-access-log-repository.interface.js';
+
+export const secretAccessLogRepositoryExtension: ISecretAccessLogRepository = {
+  async createAccessLog(
+    secretId: string,
+    userId: string,
+    action: SecretActionEnum,
+    success: boolean = true,
+    errorMessage?: string,
+  ): Promise {
+    const self = this as any;
+    const log = self.create({
+      secretId,
+      userId,
+      action,
+      success,
+      errorMessage,
+      accessedAt: new Date(),
+    });
+    return self.save(log);
+  },
+
+  async findLogsForSecret(
+    secretId: string,
+    options: { page: number; limit: number },
+  ): Promise<[SecretAccessLogEntity[], number]> {
+    const self = this as any;
+    return self.findAndCount({
+      where: {
+        secretId,
+      },
+      relations: ['user'],
+      order: {
+        accessedAt: 'DESC',
+      },
+      skip: (options.page - 1) * options.limit,
+      take: options.limit,
+    });
+  },
+};
diff --git a/backend/src/entities/secret-access-log/repository/secret-access-log-repository.interface.ts b/backend/src/entities/secret-access-log/repository/secret-access-log-repository.interface.ts
new file mode 100644
index 000000000..e3228f743
--- /dev/null
+++ b/backend/src/entities/secret-access-log/repository/secret-access-log-repository.interface.ts
@@ -0,0 +1,15 @@
+import { SecretAccessLogEntity, SecretActionEnum } from '../secret-access-log.entity.js';
+
+export interface ISecretAccessLogRepository {
+  createAccessLog(
+    secretId: string,
+    userId: string,
+    action: SecretActionEnum,
+    success?: boolean,
+    errorMessage?: string,
+  ): Promise;
+  findLogsForSecret(
+    secretId: string,
+    options: { page: number; limit: number },
+  ): Promise<[SecretAccessLogEntity[], number]>;
+}
diff --git a/backend/src/entities/secret-access-log/secret-access-log.entity.ts b/backend/src/entities/secret-access-log/secret-access-log.entity.ts
index 6ccc928a8..35cc02744 100644
--- a/backend/src/entities/secret-access-log/secret-access-log.entity.ts
+++ b/backend/src/entities/secret-access-log/secret-access-log.entity.ts
@@ -16,7 +16,7 @@ export class SecretAccessLogEntity {
   id: string;
 
   @ManyToOne(() => UserSecretEntity, (secret) => secret.accessLogs, { onDelete: 'CASCADE' })
-  @JoinColumn()
+  @JoinColumn({ name: 'secretId' })
   secret: Relation;
 
   @Column()
@@ -24,7 +24,7 @@ export class SecretAccessLogEntity {
   secretId: string;
 
   @ManyToOne(() => UserEntity, { onDelete: 'CASCADE' })
-  @JoinColumn()
+  @JoinColumn({ name: 'userId' })
   user: Relation;
 
   @Column()
diff --git a/backend/src/entities/user-secret/application/data-structures/create-secret.ds.ts b/backend/src/entities/user-secret/application/data-structures/create-secret.ds.ts
new file mode 100644
index 000000000..a4fd71abe
--- /dev/null
+++ b/backend/src/entities/user-secret/application/data-structures/create-secret.ds.ts
@@ -0,0 +1,8 @@
+export class CreateSecretDS {
+  userId: string;
+  slug: string;
+  value: string;
+  expiresAt?: string;
+  masterEncryption?: boolean;
+  masterPassword?: string;
+}
diff --git a/backend/src/entities/user-secret/application/data-structures/created-secret.ds.ts b/backend/src/entities/user-secret/application/data-structures/created-secret.ds.ts
new file mode 100644
index 000000000..b1b98bc88
--- /dev/null
+++ b/backend/src/entities/user-secret/application/data-structures/created-secret.ds.ts
@@ -0,0 +1,10 @@
+export class CreatedSecretDS {
+  id: string;
+  slug: string;
+  companyId: string;
+  createdAt: Date;
+  updatedAt: Date;
+  lastAccessedAt?: Date;
+  expiresAt?: Date;
+  masterEncryption: boolean;
+}
diff --git a/backend/src/entities/user-secret/application/data-structures/delete-secret.ds.ts b/backend/src/entities/user-secret/application/data-structures/delete-secret.ds.ts
new file mode 100644
index 000000000..1de6f1b29
--- /dev/null
+++ b/backend/src/entities/user-secret/application/data-structures/delete-secret.ds.ts
@@ -0,0 +1,9 @@
+export class DeleteSecretDS {
+  userId: string;
+  slug: string;
+}
+
+export class DeletedSecretDS {
+  message: string;
+  deletedAt: Date;
+}
diff --git a/backend/src/entities/user-secret/application/data-structures/found-secret.ds.ts b/backend/src/entities/user-secret/application/data-structures/found-secret.ds.ts
new file mode 100644
index 000000000..8e119df65
--- /dev/null
+++ b/backend/src/entities/user-secret/application/data-structures/found-secret.ds.ts
@@ -0,0 +1,11 @@
+export class FoundSecretDS {
+  id: string;
+  slug: string;
+  value?: string;
+  companyId: string;
+  createdAt: Date;
+  updatedAt: Date;
+  lastAccessedAt?: Date;
+  expiresAt?: Date;
+  masterEncryption: boolean;
+}
diff --git a/backend/src/entities/user-secret/application/data-structures/get-audit-log.ds.ts b/backend/src/entities/user-secret/application/data-structures/get-audit-log.ds.ts
new file mode 100644
index 000000000..64a9d7b35
--- /dev/null
+++ b/backend/src/entities/user-secret/application/data-structures/get-audit-log.ds.ts
@@ -0,0 +1,32 @@
+import { SecretActionEnum } from '../../../secret-access-log/secret-access-log.entity.js';
+
+export class GetAuditLogDS {
+  userId: string;
+  slug: string;
+  page: number;
+  limit: number;
+}
+
+export class AuditLogEntryDS {
+  id: string;
+  action: SecretActionEnum;
+  user: {
+    id: string;
+    email: string;
+  };
+  accessedAt: Date;
+  ipAddress?: string;
+  userAgent?: string;
+  success: boolean;
+  errorMessage?: string;
+}
+
+export class AuditLogListDS {
+  data: AuditLogEntryDS[];
+  pagination: {
+    total: number;
+    page: number;
+    limit: number;
+    totalPages: number;
+  };
+}
diff --git a/backend/src/entities/user-secret/application/data-structures/get-secret.ds.ts b/backend/src/entities/user-secret/application/data-structures/get-secret.ds.ts
new file mode 100644
index 000000000..9ad4fcd6e
--- /dev/null
+++ b/backend/src/entities/user-secret/application/data-structures/get-secret.ds.ts
@@ -0,0 +1,5 @@
+export class GetSecretDS {
+  userId: string;
+  slug: string;
+  masterPassword?: string;
+}
diff --git a/backend/src/entities/user-secret/application/data-structures/get-secrets.ds.ts b/backend/src/entities/user-secret/application/data-structures/get-secrets.ds.ts
new file mode 100644
index 000000000..1cfa962a5
--- /dev/null
+++ b/backend/src/entities/user-secret/application/data-structures/get-secrets.ds.ts
@@ -0,0 +1,27 @@
+export class GetSecretsDS {
+  userId: string;
+  page: number;
+  limit: number;
+  search?: string;
+}
+
+export class SecretListItemDS {
+  id: string;
+  slug: string;
+  companyId: string;
+  createdAt: Date;
+  updatedAt: Date;
+  lastAccessedAt?: Date;
+  expiresAt?: Date;
+  masterEncryption: boolean;
+}
+
+export class SecretsListDS {
+  data: SecretListItemDS[];
+  pagination: {
+    total: number;
+    page: number;
+    limit: number;
+    totalPages: number;
+  };
+}
diff --git a/backend/src/entities/user-secret/application/data-structures/update-secret.ds.ts b/backend/src/entities/user-secret/application/data-structures/update-secret.ds.ts
new file mode 100644
index 000000000..cefcd159b
--- /dev/null
+++ b/backend/src/entities/user-secret/application/data-structures/update-secret.ds.ts
@@ -0,0 +1,18 @@
+export class UpdateSecretDS {
+  userId: string;
+  slug: string;
+  value?: string;
+  expiresAt?: string;
+  masterPassword?: string;
+}
+
+export class UpdatedSecretDS {
+  id: string;
+  slug: string;
+  companyId: string;
+  createdAt: Date;
+  updatedAt: Date;
+  lastAccessedAt?: Date;
+  expiresAt?: Date;
+  masterEncryption: boolean;
+}
diff --git a/backend/src/entities/user-secret/application/dto/create-secret.dto.ts b/backend/src/entities/user-secret/application/dto/create-secret.dto.ts
index 0f9546293..93f8b994a 100644
--- a/backend/src/entities/user-secret/application/dto/create-secret.dto.ts
+++ b/backend/src/entities/user-secret/application/dto/create-secret.dto.ts
@@ -33,7 +33,6 @@ export class CreateSecretDto {
 
   @ApiProperty({ required: false, type: 'string' })
   @IsString()
-  @IsOptional()
   @MinLength(8)
   @ValidateIf((o) => o.masterEncryption === true)
   masterPassword?: string;
diff --git a/backend/src/entities/user-secret/repository/user-secret-repository.extension.ts b/backend/src/entities/user-secret/repository/user-secret-repository.extension.ts
new file mode 100644
index 000000000..c1a03bb52
--- /dev/null
+++ b/backend/src/entities/user-secret/repository/user-secret-repository.extension.ts
@@ -0,0 +1,38 @@
+import { Like } from 'typeorm';
+import { UserSecretEntity } from '../user-secret.entity.js';
+import { IUserSecretRepository } from './user-secret-repository.interface.js';
+
+export const userSecretRepositoryExtension: IUserSecretRepository = {
+  async findSecretBySlugAndCompanyId(slug: string, companyId: string): Promise {
+    const self = this as any;
+    return self.findOne({
+      where: {
+        slug,
+        companyId,
+      },
+    });
+  },
+
+  async findSecretsForCompany(
+    companyId: string,
+    options: { page: number; limit: number; search?: string },
+  ): Promise<[UserSecretEntity[], number]> {
+    const self = this as any;
+    const where: any = {
+      companyId,
+    };
+
+    if (options.search) {
+      where.slug = Like(`%${options.search}%`);
+    }
+
+    return self.findAndCount({
+      where,
+      order: {
+        createdAt: 'DESC',
+      },
+      skip: (options.page - 1) * options.limit,
+      take: options.limit,
+    });
+  },
+};
diff --git a/backend/src/entities/user-secret/repository/user-secret-repository.interface.ts b/backend/src/entities/user-secret/repository/user-secret-repository.interface.ts
new file mode 100644
index 000000000..5e5089818
--- /dev/null
+++ b/backend/src/entities/user-secret/repository/user-secret-repository.interface.ts
@@ -0,0 +1,9 @@
+import { UserSecretEntity } from '../user-secret.entity.js';
+
+export interface IUserSecretRepository {
+  findSecretBySlugAndCompanyId(slug: string, companyId: string): Promise;
+  findSecretsForCompany(
+    companyId: string,
+    options: { page: number; limit: number; search?: string },
+  ): Promise<[UserSecretEntity[], number]>;
+}
diff --git a/backend/src/entities/user-secret/use-cases/create-secret.use.case.ts b/backend/src/entities/user-secret/use-cases/create-secret.use.case.ts
new file mode 100644
index 000000000..556260e04
--- /dev/null
+++ b/backend/src/entities/user-secret/use-cases/create-secret.use.case.ts
@@ -0,0 +1,64 @@
+import { ConflictException, ForbiddenException, Inject, Injectable, Scope } from '@nestjs/common';
+import AbstractUseCase from '../../../common/abstract-use.case.js';
+import { BaseType } from '../../../common/data-injection.tokens.js';
+import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
+import { CreateSecretDS } from '../application/data-structures/create-secret.ds.js';
+import { CreatedSecretDS } from '../application/data-structures/created-secret.ds.js';
+import { ICreateSecret } from './user-secret-use-cases.interface.js';
+import { buildCreatedSecretDS } from '../utils/build-created-secret.ds.js';
+import { Encryptor } from '../../../helpers/encryption/encryptor.js';
+import { SecretActionEnum } from '../../secret-access-log/secret-access-log.entity.js';
+
+@Injectable({ scope: Scope.REQUEST })
+export class CreateSecretUseCase extends AbstractUseCase implements ICreateSecret {
+  constructor(
+    @Inject(BaseType.GLOBAL_DB_CONTEXT)
+    protected _dbContext: IGlobalDatabaseContext,
+  ) {
+    super();
+  }
+
+  protected async implementation(inputData: CreateSecretDS): Promise {
+    const { userId, slug, value, expiresAt, masterEncryption, masterPassword } = inputData;
+
+    const user = await this._dbContext.userRepository.findOne({
+      where: { id: userId },
+      relations: ['company'],
+    });
+
+    if (!user || !user.company) {
+      throw new ForbiddenException('User not found or not associated with a company');
+    }
+
+    const existing = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(slug, user.company.id);
+
+    if (existing) {
+      throw new ConflictException('Secret with this slug already exists in your company');
+    }
+
+    let encryptedValue = value;
+
+    if (masterEncryption && masterPassword) {
+      encryptedValue = Encryptor.encryptDataMasterPwd(encryptedValue, masterPassword);
+    }
+
+    encryptedValue = Encryptor.encryptData(encryptedValue);
+
+    const masterHash = masterPassword ? await Encryptor.hashUserPassword(masterPassword) : null;
+
+    const secret = this._dbContext.userSecretRepository.create({
+      slug,
+      encryptedValue,
+      companyId: user.company.id,
+      expiresAt: expiresAt ? new Date(expiresAt) : null,
+      masterEncryption: masterEncryption || false,
+      masterHash,
+    });
+
+    const saved = await this._dbContext.userSecretRepository.save(secret);
+
+    await this._dbContext.secretAccessLogRepository.createAccessLog(saved.id, userId, SecretActionEnum.CREATE);
+
+    return buildCreatedSecretDS(saved);
+  }
+}
diff --git a/backend/src/entities/user-secret/use-cases/delete-secret.use.case.ts b/backend/src/entities/user-secret/use-cases/delete-secret.use.case.ts
new file mode 100644
index 000000000..958eb0232
--- /dev/null
+++ b/backend/src/entities/user-secret/use-cases/delete-secret.use.case.ts
@@ -0,0 +1,45 @@
+import { ForbiddenException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
+import AbstractUseCase from '../../../common/abstract-use.case.js';
+import { BaseType } from '../../../common/data-injection.tokens.js';
+import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
+import { DeleteSecretDS, DeletedSecretDS } from '../application/data-structures/delete-secret.ds.js';
+import { IDeleteSecret } from './user-secret-use-cases.interface.js';
+import { SecretActionEnum } from '../../secret-access-log/secret-access-log.entity.js';
+
+@Injectable({ scope: Scope.REQUEST })
+export class DeleteSecretUseCase extends AbstractUseCase implements IDeleteSecret {
+  constructor(
+    @Inject(BaseType.GLOBAL_DB_CONTEXT)
+    protected _dbContext: IGlobalDatabaseContext,
+  ) {
+    super();
+  }
+
+  protected async implementation(inputData: DeleteSecretDS): Promise {
+    const { userId, slug } = inputData;
+
+    const user = await this._dbContext.userRepository.findOne({
+      where: { id: userId },
+      relations: ['company'],
+    });
+
+    if (!user || !user.company) {
+      throw new ForbiddenException('User not found or not associated with a company');
+    }
+
+    const secret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(slug, user.company.id);
+
+    if (!secret) {
+      throw new NotFoundException('Secret not found');
+    }
+
+    await this._dbContext.secretAccessLogRepository.createAccessLog(secret.id, userId, SecretActionEnum.DELETE);
+
+    await this._dbContext.userSecretRepository.remove(secret);
+
+    return {
+      message: 'Secret deleted successfully',
+      deletedAt: new Date(),
+    };
+  }
+}
diff --git a/backend/src/entities/user-secret/use-cases/get-secret-audit-log.use.case.ts b/backend/src/entities/user-secret/use-cases/get-secret-audit-log.use.case.ts
new file mode 100644
index 000000000..be915ad2f
--- /dev/null
+++ b/backend/src/entities/user-secret/use-cases/get-secret-audit-log.use.case.ts
@@ -0,0 +1,54 @@
+import { ForbiddenException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
+import AbstractUseCase from '../../../common/abstract-use.case.js';
+import { BaseType } from '../../../common/data-injection.tokens.js';
+import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
+import { AuditLogListDS, GetAuditLogDS } from '../application/data-structures/get-audit-log.ds.js';
+import { IGetSecretAuditLog } from './user-secret-use-cases.interface.js';
+import { buildAuditLogEntryDS } from '../utils/build-audit-log-entry.ds.js';
+
+@Injectable({ scope: Scope.REQUEST })
+export class GetSecretAuditLogUseCase
+  extends AbstractUseCase
+  implements IGetSecretAuditLog
+{
+  constructor(
+    @Inject(BaseType.GLOBAL_DB_CONTEXT)
+    protected _dbContext: IGlobalDatabaseContext,
+  ) {
+    super();
+  }
+
+  protected async implementation(inputData: GetAuditLogDS): Promise {
+    const { userId, slug, page, limit } = inputData;
+
+    const user = await this._dbContext.userRepository.findOne({
+      where: { id: userId },
+      relations: ['company'],
+    });
+
+    if (!user || !user.company) {
+      throw new ForbiddenException('User not found or not associated with a company');
+    }
+
+    const secret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(slug, user.company.id);
+
+    if (!secret) {
+      throw new NotFoundException('Secret not found');
+    }
+
+    const [logs, total] = await this._dbContext.secretAccessLogRepository.findLogsForSecret(secret.id, {
+      page,
+      limit,
+    });
+
+    return {
+      data: logs.map((log) => buildAuditLogEntryDS(log)),
+      pagination: {
+        total,
+        page,
+        limit,
+        totalPages: Math.ceil(total / limit),
+      },
+    };
+  }
+}
diff --git a/backend/src/entities/user-secret/use-cases/get-secret-by-slug.use.case.ts b/backend/src/entities/user-secret/use-cases/get-secret-by-slug.use.case.ts
new file mode 100644
index 000000000..1a029c579
--- /dev/null
+++ b/backend/src/entities/user-secret/use-cases/get-secret-by-slug.use.case.ts
@@ -0,0 +1,74 @@
+import { ForbiddenException, GoneException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
+import AbstractUseCase from '../../../common/abstract-use.case.js';
+import { BaseType } from '../../../common/data-injection.tokens.js';
+import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
+import { GetSecretDS } from '../application/data-structures/get-secret.ds.js';
+import { FoundSecretDS } from '../application/data-structures/found-secret.ds.js';
+import { IGetSecretBySlug } from './user-secret-use-cases.interface.js';
+import { buildFoundSecretDS } from '../utils/build-found-secret.ds.js';
+import { Encryptor } from '../../../helpers/encryption/encryptor.js';
+import { SecretActionEnum } from '../../secret-access-log/secret-access-log.entity.js';
+
+@Injectable({ scope: Scope.REQUEST })
+export class GetSecretBySlugUseCase extends AbstractUseCase implements IGetSecretBySlug {
+  constructor(
+    @Inject(BaseType.GLOBAL_DB_CONTEXT)
+    protected _dbContext: IGlobalDatabaseContext,
+  ) {
+    super();
+  }
+
+  protected async implementation(inputData: GetSecretDS): Promise {
+    const { userId, slug, masterPassword } = inputData;
+
+    const user = await this._dbContext.userRepository.findOne({
+      where: { id: userId },
+      relations: ['company'],
+    });
+
+    if (!user || !user.company) {
+      throw new ForbiddenException('User not found or not associated with a company');
+    }
+
+    const secret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(slug, user.company.id);
+
+    if (!secret) {
+      throw new NotFoundException('Secret not found');
+    }
+
+    if (secret.expiresAt && secret.expiresAt < new Date()) {
+      throw new GoneException('Secret has expired');
+    }
+
+    if (secret.masterEncryption && !masterPassword) {
+      throw new ForbiddenException('Master password required');
+    }
+
+    if (secret.masterEncryption && masterPassword) {
+      const isValid = await Encryptor.verifyUserPassword(masterPassword, secret.masterHash);
+      if (!isValid) {
+        await this._dbContext.secretAccessLogRepository.createAccessLog(
+          secret.id,
+          userId,
+          SecretActionEnum.VIEW,
+          false,
+          'Invalid master password',
+        );
+        throw new ForbiddenException('Invalid master password');
+      }
+    }
+
+    secret.lastAccessedAt = new Date();
+    await this._dbContext.userSecretRepository.save(secret);
+
+    await this._dbContext.secretAccessLogRepository.createAccessLog(secret.id, userId, SecretActionEnum.VIEW);
+
+    let decryptedValue = Encryptor.decryptData(secret.encryptedValue);
+
+    if (secret.masterEncryption && masterPassword) {
+      decryptedValue = Encryptor.decryptDataMasterPwd(decryptedValue, masterPassword);
+    }
+
+    return buildFoundSecretDS(secret, decryptedValue);
+  }
+}
diff --git a/backend/src/entities/user-secret/use-cases/get-secrets.use.case.ts b/backend/src/entities/user-secret/use-cases/get-secrets.use.case.ts
new file mode 100644
index 000000000..987558f01
--- /dev/null
+++ b/backend/src/entities/user-secret/use-cases/get-secrets.use.case.ts
@@ -0,0 +1,46 @@
+import { ForbiddenException, Inject, Injectable, Scope } from '@nestjs/common';
+import AbstractUseCase from '../../../common/abstract-use.case.js';
+import { BaseType } from '../../../common/data-injection.tokens.js';
+import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
+import { GetSecretsDS, SecretsListDS } from '../application/data-structures/get-secrets.ds.js';
+import { IGetSecrets } from './user-secret-use-cases.interface.js';
+import { buildSecretListItemDS } from '../utils/build-secret-list-item.ds.js';
+
+@Injectable({ scope: Scope.REQUEST })
+export class GetSecretsUseCase extends AbstractUseCase implements IGetSecrets {
+  constructor(
+    @Inject(BaseType.GLOBAL_DB_CONTEXT)
+    protected _dbContext: IGlobalDatabaseContext,
+  ) {
+    super();
+  }
+
+  protected async implementation(inputData: GetSecretsDS): Promise {
+    const { userId, page, limit, search } = inputData;
+
+    const user = await this._dbContext.userRepository.findOne({
+      where: { id: userId },
+      relations: ['company'],
+    });
+
+    if (!user || !user.company) {
+      throw new ForbiddenException('User not found or not associated with a company');
+    }
+
+    const [secrets, total] = await this._dbContext.userSecretRepository.findSecretsForCompany(user.company.id, {
+      page,
+      limit,
+      search,
+    });
+
+    return {
+      data: secrets.map((secret) => buildSecretListItemDS(secret)),
+      pagination: {
+        total,
+        page,
+        limit,
+        totalPages: Math.ceil(total / limit),
+      },
+    };
+  }
+}
diff --git a/backend/src/entities/user-secret/use-cases/update-secret.use.case.ts b/backend/src/entities/user-secret/use-cases/update-secret.use.case.ts
new file mode 100644
index 000000000..3c94620fe
--- /dev/null
+++ b/backend/src/entities/user-secret/use-cases/update-secret.use.case.ts
@@ -0,0 +1,81 @@
+import { ForbiddenException, GoneException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
+import AbstractUseCase from '../../../common/abstract-use.case.js';
+import { BaseType } from '../../../common/data-injection.tokens.js';
+import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
+import { UpdateSecretDS, UpdatedSecretDS } from '../application/data-structures/update-secret.ds.js';
+import { IUpdateSecret } from './user-secret-use-cases.interface.js';
+import { buildUpdatedSecretDS } from '../utils/build-updated-secret.ds.js';
+import { Encryptor } from '../../../helpers/encryption/encryptor.js';
+import { SecretActionEnum } from '../../secret-access-log/secret-access-log.entity.js';
+
+@Injectable({ scope: Scope.REQUEST })
+export class UpdateSecretUseCase extends AbstractUseCase implements IUpdateSecret {
+  constructor(
+    @Inject(BaseType.GLOBAL_DB_CONTEXT)
+    protected _dbContext: IGlobalDatabaseContext,
+  ) {
+    super();
+  }
+
+  protected async implementation(inputData: UpdateSecretDS): Promise {
+    const { userId, slug, value, expiresAt, masterPassword } = inputData;
+
+    const user = await this._dbContext.userRepository.findOne({
+      where: { id: userId },
+      relations: ['company'],
+    });
+
+    if (!user || !user.company) {
+      throw new ForbiddenException('User not found or not associated with a company');
+    }
+
+    const secret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(slug, user.company.id);
+
+    if (!secret) {
+      throw new NotFoundException('Secret not found');
+    }
+
+    if (secret.expiresAt && secret.expiresAt < new Date()) {
+      throw new GoneException('Secret has expired');
+    }
+
+    if (secret.masterEncryption && !masterPassword) {
+      throw new ForbiddenException('Master password required');
+    }
+
+    if (secret.masterEncryption && masterPassword) {
+      const isValid = await Encryptor.verifyUserPassword(masterPassword, secret.masterHash);
+      if (!isValid) {
+        await this._dbContext.secretAccessLogRepository.createAccessLog(
+          secret.id,
+          userId,
+          SecretActionEnum.UPDATE,
+          false,
+          'Invalid master password',
+        );
+        throw new ForbiddenException('Invalid master password');
+      }
+    }
+
+    if (value) {
+      let encryptedValue = value;
+
+      if (secret.masterEncryption && masterPassword) {
+        encryptedValue = Encryptor.encryptDataMasterPwd(encryptedValue, masterPassword);
+      }
+
+      encryptedValue = Encryptor.encryptData(encryptedValue);
+      secret.encryptedValue = encryptedValue;
+    }
+
+    if (expiresAt !== undefined) {
+      secret.expiresAt = expiresAt ? new Date(expiresAt) : null;
+    }
+
+    const updated = await this._dbContext.userSecretRepository.save(secret);
+
+    await this._dbContext.secretAccessLogRepository.createAccessLog(secret.id, userId, SecretActionEnum.UPDATE);
+
+    return buildUpdatedSecretDS(updated);
+  }
+}
diff --git a/backend/src/entities/user-secret/use-cases/user-secret-use-cases.interface.ts b/backend/src/entities/user-secret/use-cases/user-secret-use-cases.interface.ts
new file mode 100644
index 000000000..ee1472aeb
--- /dev/null
+++ b/backend/src/entities/user-secret/use-cases/user-secret-use-cases.interface.ts
@@ -0,0 +1,33 @@
+import { InTransactionEnum } from '../../../enums/in-transaction.enum.js';
+import { CreateSecretDS } from '../application/data-structures/create-secret.ds.js';
+import { CreatedSecretDS } from '../application/data-structures/created-secret.ds.js';
+import { DeleteSecretDS, DeletedSecretDS } from '../application/data-structures/delete-secret.ds.js';
+import { FoundSecretDS } from '../application/data-structures/found-secret.ds.js';
+import { AuditLogListDS, GetAuditLogDS } from '../application/data-structures/get-audit-log.ds.js';
+import { GetSecretDS } from '../application/data-structures/get-secret.ds.js';
+import { GetSecretsDS, SecretsListDS } from '../application/data-structures/get-secrets.ds.js';
+import { UpdatedSecretDS, UpdateSecretDS } from '../application/data-structures/update-secret.ds.js';
+
+export interface ICreateSecret {
+  execute(inputData: CreateSecretDS, inTransaction: InTransactionEnum): Promise;
+}
+
+export interface IGetSecrets {
+  execute(inputData: GetSecretsDS): Promise;
+}
+
+export interface IGetSecretBySlug {
+  execute(inputData: GetSecretDS): Promise;
+}
+
+export interface IUpdateSecret {
+  execute(inputData: UpdateSecretDS, inTransaction: InTransactionEnum): Promise;
+}
+
+export interface IDeleteSecret {
+  execute(inputData: DeleteSecretDS, inTransaction: InTransactionEnum): Promise;
+}
+
+export interface IGetSecretAuditLog {
+  execute(inputData: GetAuditLogDS): Promise;
+}
diff --git a/backend/src/entities/user-secret/user-secret.controller.ts b/backend/src/entities/user-secret/user-secret.controller.ts
index bf8c9cfa2..7c53403d2 100644
--- a/backend/src/entities/user-secret/user-secret.controller.ts
+++ b/backend/src/entities/user-secret/user-secret.controller.ts
@@ -5,6 +5,7 @@ import {
   Get,
   HttpCode,
   HttpStatus,
+  Inject,
   Injectable,
   Param,
   Post,
@@ -16,12 +17,26 @@ import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@ne
 import { UserId } from '../../decorators/user-id.decorator.js';
 import { MasterPassword } from '../../decorators/master-password.decorator.js';
 import { SentryInterceptor } from '../../interceptors/sentry.interceptor.js';
+import { UseCaseType } from '../../common/data-injection.tokens.js';
+import { InTransactionEnum } from '../../enums/in-transaction.enum.js';
 import { CreateSecretDto } from './application/dto/create-secret.dto.js';
 import { UpdateSecretDto } from './application/dto/update-secret.dto.js';
 import { FoundSecretDto } from './application/dto/found-secret.dto.js';
 import { SecretListResponseDto } from './application/dto/secret-list.dto.js';
 import { AuditLogResponseDto } from './application/dto/audit-log.dto.js';
-import { UserSecretsService } from './user-secrets.service.js';
+import {
+  ICreateSecret,
+  IDeleteSecret,
+  IGetSecretAuditLog,
+  IGetSecretBySlug,
+  IGetSecrets,
+  IUpdateSecret,
+} from './use-cases/user-secret-use-cases.interface.js';
+import { buildCreatedSecretDto } from './utils/build-created-secret.dto.js';
+import { buildFoundSecretDto } from './utils/build-found-secret.dto.js';
+import { buildUpdatedSecretDto } from './utils/build-updated-secret.dto.js';
+import { buildSecretListResponseDto } from './utils/build-secret-list.dto.js';
+import { buildAuditLogResponseDto } from './utils/build-audit-log.dto.js';
 
 @UseInterceptors(SentryInterceptor)
 @Controller()
@@ -29,7 +44,20 @@ import { UserSecretsService } from './user-secrets.service.js';
 @ApiTags('Secrets')
 @Injectable()
 export class UserSecretController {
-  constructor(private readonly userSecretsService: UserSecretsService) {}
+  constructor(
+    @Inject(UseCaseType.CREATE_SECRET)
+    private readonly createSecretUseCase: ICreateSecret,
+    @Inject(UseCaseType.GET_SECRETS)
+    private readonly getSecretsUseCase: IGetSecrets,
+    @Inject(UseCaseType.GET_SECRET_BY_SLUG)
+    private readonly getSecretBySlugUseCase: IGetSecretBySlug,
+    @Inject(UseCaseType.UPDATE_SECRET)
+    private readonly updateSecretUseCase: IUpdateSecret,
+    @Inject(UseCaseType.DELETE_SECRET)
+    private readonly deleteSecretUseCase: IDeleteSecret,
+    @Inject(UseCaseType.GET_SECRET_AUDIT_LOG)
+    private readonly getSecretAuditLogUseCase: IGetSecretAuditLog,
+  ) {}
 
   @ApiOperation({ summary: 'Create new secret' })
   @ApiResponse({
@@ -44,7 +72,18 @@ export class UserSecretController {
   @Post('/secrets')
   @HttpCode(HttpStatus.CREATED)
   async createSecret(@UserId() userId: string, @Body() createDto: CreateSecretDto): Promise {
-    return await this.userSecretsService.createSecret(userId, createDto);
+    const createdSecret = await this.createSecretUseCase.execute(
+      {
+        userId,
+        slug: createDto.slug,
+        value: createDto.value,
+        expiresAt: createDto.expiresAt,
+        masterEncryption: createDto.masterEncryption,
+        masterPassword: createDto.masterPassword,
+      },
+      InTransactionEnum.ON,
+    );
+    return buildCreatedSecretDto(createdSecret);
   }
 
   @ApiOperation({ summary: 'Get all company secrets' })
@@ -63,11 +102,13 @@ export class UserSecretController {
     @Query('limit') limit?: number,
     @Query('search') search?: string,
   ): Promise {
-    return await this.userSecretsService.getSecrets(userId, {
+    const secretsList = await this.getSecretsUseCase.execute({
+      userId,
       page: page || 1,
       limit: limit || 20,
       search,
     });
+    return buildSecretListResponseDto(secretsList);
   }
 
   @ApiOperation({ summary: 'Get secret by slug' })
@@ -94,7 +135,12 @@ export class UserSecretController {
     @Param('slug') slug: string,
     @MasterPassword() masterPassword?: string,
   ): Promise {
-    return await this.userSecretsService.getSecretBySlug(userId, slug, masterPassword);
+    const foundSecret = await this.getSecretBySlugUseCase.execute({
+      userId,
+      slug,
+      masterPassword,
+    });
+    return buildFoundSecretDto(foundSecret);
   }
 
   @ApiOperation({ summary: 'Update secret' })
@@ -105,7 +151,7 @@ export class UserSecretController {
   })
   @ApiResponse({
     status: 403,
-    description: 'You don\'t have permission to modify this secret.',
+    description: "You don't have permission to modify this secret.",
   })
   @ApiResponse({
     status: 404,
@@ -118,7 +164,17 @@ export class UserSecretController {
     @Body() updateDto: UpdateSecretDto,
     @MasterPassword() masterPassword?: string,
   ): Promise {
-    return await this.userSecretsService.updateSecret(userId, slug, updateDto, masterPassword);
+    const updatedSecret = await this.updateSecretUseCase.execute(
+      {
+        userId,
+        slug,
+        value: updateDto.value,
+        expiresAt: updateDto.expiresAt,
+        masterPassword,
+      },
+      InTransactionEnum.ON,
+    );
+    return buildUpdatedSecretDto(updatedSecret);
   }
 
   @ApiOperation({ summary: 'Delete secret' })
@@ -128,7 +184,7 @@ export class UserSecretController {
   })
   @ApiResponse({
     status: 403,
-    description: 'You don\'t have permission to delete this secret.',
+    description: "You don't have permission to delete this secret.",
   })
   @ApiResponse({
     status: 404,
@@ -136,7 +192,7 @@ export class UserSecretController {
   })
   @Delete('/secrets/:slug')
   async deleteSecret(@UserId() userId: string, @Param('slug') slug: string): Promise<{ message: string; deletedAt: Date }> {
-    return await this.userSecretsService.deleteSecret(userId, slug);
+    return await this.deleteSecretUseCase.execute({ userId, slug }, InTransactionEnum.ON);
   }
 
   @ApiOperation({ summary: 'Get secret audit log' })
@@ -147,7 +203,7 @@ export class UserSecretController {
   })
   @ApiResponse({
     status: 403,
-    description: 'You don\'t have permission to view this audit log.',
+    description: "You don't have permission to view this audit log.",
   })
   @ApiResponse({
     status: 404,
@@ -162,9 +218,12 @@ export class UserSecretController {
     @Query('page') page?: number,
     @Query('limit') limit?: number,
   ): Promise {
-    return await this.userSecretsService.getAuditLog(userId, slug, {
+    const auditLog = await this.getSecretAuditLogUseCase.execute({
+      userId,
+      slug,
       page: page || 1,
       limit: limit || 50,
     });
+    return buildAuditLogResponseDto(auditLog);
   }
 }
diff --git a/backend/src/entities/user-secret/user-secret.entity.ts b/backend/src/entities/user-secret/user-secret.entity.ts
index 55ca935ff..89ea242a5 100644
--- a/backend/src/entities/user-secret/user-secret.entity.ts
+++ b/backend/src/entities/user-secret/user-secret.entity.ts
@@ -20,7 +20,7 @@ export class UserSecretEntity {
   id: string;
 
   @ManyToOne(() => CompanyInfoEntity, { onDelete: 'CASCADE' })
-  @JoinColumn()
+  @JoinColumn({ name: 'companyId' })
   company: Relation;
 
   @Column()
diff --git a/backend/src/entities/user-secret/user-secret.module.ts b/backend/src/entities/user-secret/user-secret.module.ts
index 173ae03d9..aa5f1bb8d 100644
--- a/backend/src/entities/user-secret/user-secret.module.ts
+++ b/backend/src/entities/user-secret/user-secret.module.ts
@@ -4,15 +4,50 @@ import { UserSecretEntity } from './user-secret.entity.js';
 import { SecretAccessLogEntity } from '../secret-access-log/secret-access-log.entity.js';
 import { UserEntity } from '../user/user.entity.js';
 import { UserSecretController } from './user-secret.controller.js';
-import { UserSecretsService } from './user-secrets.service.js';
 import { AuthMiddleware } from '../../authorization/auth.middleware.js';
 import { LogOutEntity } from '../log-out/log-out.entity.js';
+import { GlobalDatabaseContext } from '../../common/application/global-database-context.js';
+import { BaseType, UseCaseType } from '../../common/data-injection.tokens.js';
+import { CreateSecretUseCase } from './use-cases/create-secret.use.case.js';
+import { GetSecretsUseCase } from './use-cases/get-secrets.use.case.js';
+import { GetSecretBySlugUseCase } from './use-cases/get-secret-by-slug.use.case.js';
+import { UpdateSecretUseCase } from './use-cases/update-secret.use.case.js';
+import { DeleteSecretUseCase } from './use-cases/delete-secret.use.case.js';
+import { GetSecretAuditLogUseCase } from './use-cases/get-secret-audit-log.use.case.js';
 
 @Module({
   imports: [TypeOrmModule.forFeature([UserSecretEntity, SecretAccessLogEntity, UserEntity, LogOutEntity])],
-  providers: [UserSecretsService],
+  providers: [
+    {
+      provide: BaseType.GLOBAL_DB_CONTEXT,
+      useClass: GlobalDatabaseContext,
+    },
+    {
+      provide: UseCaseType.CREATE_SECRET,
+      useClass: CreateSecretUseCase,
+    },
+    {
+      provide: UseCaseType.GET_SECRETS,
+      useClass: GetSecretsUseCase,
+    },
+    {
+      provide: UseCaseType.GET_SECRET_BY_SLUG,
+      useClass: GetSecretBySlugUseCase,
+    },
+    {
+      provide: UseCaseType.UPDATE_SECRET,
+      useClass: UpdateSecretUseCase,
+    },
+    {
+      provide: UseCaseType.DELETE_SECRET,
+      useClass: DeleteSecretUseCase,
+    },
+    {
+      provide: UseCaseType.GET_SECRET_AUDIT_LOG,
+      useClass: GetSecretAuditLogUseCase,
+    },
+  ],
   controllers: [UserSecretController],
-  exports: [UserSecretsService],
 })
 export class UserSecretModule implements NestModule {
   public configure(consumer: MiddlewareConsumer): void {
diff --git a/backend/src/entities/user-secret/user-secrets.service.spec.ts b/backend/src/entities/user-secret/user-secrets.service.spec.ts
deleted file mode 100644
index 717906577..000000000
--- a/backend/src/entities/user-secret/user-secrets.service.spec.ts
+++ /dev/null
@@ -1,280 +0,0 @@
-import { Test, TestingModule } from '@nestjs/testing';
-import { Repository } from 'typeorm';
-import { getRepositoryToken } from '@nestjs/typeorm';
-import { ConflictException, ForbiddenException, GoneException, NotFoundException } from '@nestjs/common';
-import { UserSecretsService } from './user-secrets.service.js';
-import { UserSecretEntity } from './user-secret.entity.js';
-import { SecretAccessLogEntity, SecretActionEnum } from '../secret-access-log/secret-access-log.entity.js';
-import { UserEntity } from '../user/user.entity.js';
-import { CompanyInfoEntity } from '../company-info/company-info.entity.js';
-
-describe('UserSecretsService', () => {
-  let service: UserSecretsService;
-  let secretRepository: Repository;
-  let auditLogRepository: Repository;
-  let userRepository: Repository;
-
-  const mockCompany: CompanyInfoEntity = {
-    id: 'company-uuid-1',
-    name: 'Test Company',
-  } as CompanyInfoEntity;
-
-  const mockUser: UserEntity = {
-    id: 'user-uuid-1',
-    email: 'test@example.com',
-    company: mockCompany,
-  } as UserEntity;
-
-  const mockSecret: UserSecretEntity = {
-    id: 'secret-uuid-1',
-    slug: 'test-secret',
-    encryptedValue: 'encrypted-value',
-    companyId: 'company-uuid-1',
-    company: mockCompany,
-    createdAt: new Date('2025-01-01'),
-    updatedAt: new Date('2025-01-01'),
-    lastAccessedAt: null,
-    expiresAt: null,
-    masterEncryption: false,
-    masterHash: null,
-    accessLogs: [],
-  };
-
-  beforeEach(async () => {
-    const module: TestingModule = await Test.createTestingModule({
-      providers: [
-        UserSecretsService,
-        {
-          provide: getRepositoryToken(UserSecretEntity),
-          useValue: {
-            findOne: jest.fn(),
-            findAndCount: jest.fn(),
-            create: jest.fn(),
-            save: jest.fn(),
-            remove: jest.fn(),
-          },
-        },
-        {
-          provide: getRepositoryToken(SecretAccessLogEntity),
-          useValue: {
-            create: jest.fn(),
-            save: jest.fn(),
-            findAndCount: jest.fn(),
-          },
-        },
-        {
-          provide: getRepositoryToken(UserEntity),
-          useValue: {
-            findOne: jest.fn(),
-          },
-        },
-      ],
-    }).compile();
-
-    service = module.get(UserSecretsService);
-    secretRepository = module.get>(getRepositoryToken(UserSecretEntity));
-    auditLogRepository = module.get>(getRepositoryToken(SecretAccessLogEntity));
-    userRepository = module.get>(getRepositoryToken(UserEntity));
-  });
-
-  describe('createSecret', () => {
-    it('should create a new secret successfully', async () => {
-      const createDto = {
-        slug: 'new-secret',
-        value: 'secret-value',
-        masterEncryption: false,
-      };
-
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(null); // No existing secret
-      jest.spyOn(secretRepository, 'create').mockReturnValue(mockSecret);
-      jest.spyOn(secretRepository, 'save').mockResolvedValue(mockSecret);
-      jest.spyOn(auditLogRepository, 'create').mockReturnValue({} as SecretAccessLogEntity);
-      jest.spyOn(auditLogRepository, 'save').mockResolvedValue({} as SecretAccessLogEntity);
-
-      const result = await service.createSecret('user-uuid-1', createDto);
-
-      expect(result.slug).toBe('test-secret');
-      expect(result.companyId).toBe('company-uuid-1');
-      expect(userRepository.findOne).toHaveBeenCalledWith({
-        where: { id: 'user-uuid-1' },
-        relations: ['company'],
-      });
-    });
-
-    it('should throw ConflictException if slug already exists', async () => {
-      const createDto = {
-        slug: 'existing-secret',
-        value: 'secret-value',
-      };
-
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(mockSecret); // Existing secret
-
-      await expect(service.createSecret('user-uuid-1', createDto)).rejects.toThrow(ConflictException);
-    });
-
-    it('should throw ForbiddenException if user has no company', async () => {
-      const createDto = {
-        slug: 'new-secret',
-        value: 'secret-value',
-      };
-
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue({ ...mockUser, company: null } as UserEntity);
-
-      await expect(service.createSecret('user-uuid-1', createDto)).rejects.toThrow(ForbiddenException);
-    });
-  });
-
-  describe('getSecretBySlug', () => {
-    it('should return secret with decrypted value', async () => {
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(mockSecret);
-      jest.spyOn(secretRepository, 'save').mockResolvedValue(mockSecret);
-      jest.spyOn(auditLogRepository, 'create').mockReturnValue({} as SecretAccessLogEntity);
-      jest.spyOn(auditLogRepository, 'save').mockResolvedValue({} as SecretAccessLogEntity);
-
-      const result = await service.getSecretBySlug('user-uuid-1', 'test-secret');
-
-      expect(result.slug).toBe('test-secret');
-      expect(result.value).toBe('encrypted-value'); // TODO: Should be decrypted
-      expect(auditLogRepository.save).toHaveBeenCalled();
-    });
-
-    it('should throw NotFoundException if secret not found', async () => {
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(null);
-
-      await expect(service.getSecretBySlug('user-uuid-1', 'non-existent')).rejects.toThrow(NotFoundException);
-    });
-
-    it('should throw GoneException if secret is expired', async () => {
-      const expiredSecret = { ...mockSecret, expiresAt: new Date('2020-01-01') };
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(expiredSecret);
-
-      await expect(service.getSecretBySlug('user-uuid-1', 'test-secret')).rejects.toThrow(GoneException);
-    });
-
-    it('should throw ForbiddenException if master password required but not provided', async () => {
-      const protectedSecret = { ...mockSecret, masterEncryption: true };
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(protectedSecret);
-
-      await expect(service.getSecretBySlug('user-uuid-1', 'test-secret')).rejects.toThrow(ForbiddenException);
-    });
-  });
-
-  describe('updateSecret', () => {
-    it('should update secret value successfully', async () => {
-      const updateDto = {
-        value: 'new-value',
-      };
-
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(mockSecret);
-      jest.spyOn(secretRepository, 'save').mockResolvedValue({ ...mockSecret, encryptedValue: 'new-value' });
-      jest.spyOn(auditLogRepository, 'create').mockReturnValue({} as SecretAccessLogEntity);
-      jest.spyOn(auditLogRepository, 'save').mockResolvedValue({} as SecretAccessLogEntity);
-
-      const result = await service.updateSecret('user-uuid-1', 'test-secret', updateDto);
-
-      expect(secretRepository.save).toHaveBeenCalled();
-      expect(auditLogRepository.save).toHaveBeenCalled();
-    });
-
-    it('should throw NotFoundException if secret not found', async () => {
-      const updateDto = { value: 'new-value' };
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(null);
-
-      await expect(service.updateSecret('user-uuid-1', 'non-existent', updateDto)).rejects.toThrow(NotFoundException);
-    });
-  });
-
-  describe('deleteSecret', () => {
-    it('should delete secret successfully', async () => {
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(mockSecret);
-      jest.spyOn(secretRepository, 'remove').mockResolvedValue(mockSecret);
-      jest.spyOn(auditLogRepository, 'create').mockReturnValue({} as SecretAccessLogEntity);
-      jest.spyOn(auditLogRepository, 'save').mockResolvedValue({} as SecretAccessLogEntity);
-
-      const result = await service.deleteSecret('user-uuid-1', 'test-secret');
-
-      expect(result.message).toBe('Secret deleted successfully');
-      expect(secretRepository.remove).toHaveBeenCalledWith(mockSecret);
-      expect(auditLogRepository.save).toHaveBeenCalled();
-    });
-
-    it('should throw NotFoundException if secret not found', async () => {
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(null);
-
-      await expect(service.deleteSecret('user-uuid-1', 'non-existent')).rejects.toThrow(NotFoundException);
-    });
-  });
-
-  describe('getSecrets', () => {
-    it('should return paginated list of secrets', async () => {
-      const mockSecrets = [mockSecret];
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findAndCount').mockResolvedValue([mockSecrets, 1]);
-
-      const result = await service.getSecrets('user-uuid-1', { page: 1, limit: 20 });
-
-      expect(result.data).toHaveLength(1);
-      expect(result.pagination.total).toBe(1);
-      expect(result.pagination.page).toBe(1);
-      expect(result.pagination.totalPages).toBe(1);
-    });
-
-    it('should filter secrets by search term', async () => {
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findAndCount').mockResolvedValue([[], 0]);
-
-      await service.getSecrets('user-uuid-1', { page: 1, limit: 20, search: 'test' });
-
-      expect(secretRepository.findAndCount).toHaveBeenCalledWith(
-        expect.objectContaining({
-          where: expect.objectContaining({
-            slug: expect.anything(),
-          }),
-        }),
-      );
-    });
-  });
-
-  describe('getAuditLog', () => {
-    it('should return paginated audit log', async () => {
-      const mockLog: SecretAccessLogEntity = {
-        id: 'log-uuid-1',
-        secretId: 'secret-uuid-1',
-        userId: 'user-uuid-1',
-        user: mockUser,
-        action: SecretActionEnum.VIEW,
-        accessedAt: new Date(),
-        success: true,
-      } as SecretAccessLogEntity;
-
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(mockSecret);
-      jest.spyOn(auditLogRepository, 'findAndCount').mockResolvedValue([[mockLog], 1]);
-
-      const result = await service.getAuditLog('user-uuid-1', 'test-secret', { page: 1, limit: 50 });
-
-      expect(result.data).toHaveLength(1);
-      expect(result.data[0].action).toBe(SecretActionEnum.VIEW);
-      expect(result.pagination.total).toBe(1);
-    });
-
-    it('should throw NotFoundException if secret not found', async () => {
-      jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
-      jest.spyOn(secretRepository, 'findOne').mockResolvedValue(null);
-
-      await expect(service.getAuditLog('user-uuid-1', 'non-existent', { page: 1, limit: 50 })).rejects.toThrow(
-        NotFoundException,
-      );
-    });
-  });
-});
diff --git a/backend/src/entities/user-secret/user-secrets.service.ts b/backend/src/entities/user-secret/user-secrets.service.ts
deleted file mode 100644
index 3f5d235ae..000000000
--- a/backend/src/entities/user-secret/user-secrets.service.ts
+++ /dev/null
@@ -1,379 +0,0 @@
-import {
-  ConflictException,
-  ForbiddenException,
-  GoneException,
-  Injectable,
-  NotFoundException,
-} from '@nestjs/common';
-import { InjectRepository } from '@nestjs/typeorm';
-import { Repository, Like } from 'typeorm';
-import { UserSecretEntity } from './user-secret.entity.js';
-import { SecretAccessLogEntity, SecretActionEnum } from '../secret-access-log/secret-access-log.entity.js';
-import { UserEntity } from '../user/user.entity.js';
-import { CreateSecretDto } from './application/dto/create-secret.dto.js';
-import { UpdateSecretDto } from './application/dto/update-secret.dto.js';
-import { FoundSecretDto } from './application/dto/found-secret.dto.js';
-import { SecretListResponseDto, SecretListItemDto } from './application/dto/secret-list.dto.js';
-import { AuditLogResponseDto, AuditLogEntryDto } from './application/dto/audit-log.dto.js';
-import { Encryptor } from '../../helpers/encryption/encryptor.js';
-
-@Injectable()
-export class UserSecretsService {
-  constructor(
-    @InjectRepository(UserSecretEntity)
-    private readonly secretRepository: Repository,
-    @InjectRepository(SecretAccessLogEntity)
-    private readonly auditLogRepository: Repository,
-    @InjectRepository(UserEntity)
-    private readonly userRepository: Repository,
-  ) {}
-
-  private async getUserWithCompany(userId: string): Promise {
-    const user = await this.userRepository.findOne({
-      where: { id: userId },
-      relations: ['company'],
-    });
-
-    if (!user || !user.company) {
-      throw new ForbiddenException('User not found or not associated with a company');
-    }
-
-    return user;
-  }
-
-  private async logAccess(
-    secretId: string,
-    userId: string,
-    action: SecretActionEnum,
-    success: boolean = true,
-    errorMessage?: string,
-  ): Promise {
-    const log = this.auditLogRepository.create({
-      secretId,
-      userId,
-      action,
-      success,
-      errorMessage,
-      accessedAt: new Date(),
-    });
-
-    await this.auditLogRepository.save(log);
-  }
-
-  async createSecret(userId: string, createDto: CreateSecretDto): Promise {
-    const user = await this.getUserWithCompany(userId);
-
-    // Check if slug already exists in company
-    const existing = await this.secretRepository.findOne({
-      where: {
-        slug: createDto.slug,
-        companyId: user.company.id,
-      },
-    });
-
-    if (existing) {
-      throw new ConflictException('Secret with this slug already exists in your company');
-    }
-
-    // Encrypt the secret value
-    let encryptedValue = createDto.value;
-
-    // Layer 2: Encrypt with master password first (if enabled)
-    if (createDto.masterEncryption && createDto.masterPassword) {
-      encryptedValue = Encryptor.encryptDataMasterPwd(encryptedValue, createDto.masterPassword);
-    }
-
-    // Layer 1: Encrypt with PRIVATE_KEY (always)
-    encryptedValue = Encryptor.encryptData(encryptedValue);
-
-    // Hash master password if provided
-    const masterHash = createDto.masterPassword ? await Encryptor.hashUserPassword(createDto.masterPassword) : null;
-
-    // Create secret
-    const secret = this.secretRepository.create({
-      slug: createDto.slug,
-      encryptedValue,
-      companyId: user.company.id,
-      expiresAt: createDto.expiresAt ? new Date(createDto.expiresAt) : null,
-      masterEncryption: createDto.masterEncryption || false,
-      masterHash,
-    });
-
-    const saved = await this.secretRepository.save(secret);
-
-    // Log creation
-    await this.logAccess(saved.id, userId, SecretActionEnum.CREATE);
-
-    return this.buildFoundSecretDto(saved, false);
-  }
-
-  async getSecrets(
-    userId: string,
-    options: { page: number; limit: number; search?: string },
-  ): Promise {
-    const user = await this.getUserWithCompany(userId);
-
-    const where: any = {
-      companyId: user.company.id,
-    };
-
-    if (options.search) {
-      where.slug = Like(`%${options.search}%`);
-    }
-
-    const [secrets, total] = await this.secretRepository.findAndCount({
-      where,
-      order: {
-        createdAt: 'DESC',
-      },
-      skip: (options.page - 1) * options.limit,
-      take: options.limit,
-    });
-
-    return {
-      data: secrets.map((secret) => this.buildSecretListItemDto(secret)),
-      pagination: {
-        total,
-        page: options.page,
-        limit: options.limit,
-        totalPages: Math.ceil(total / options.limit),
-      },
-    };
-  }
-
-  async getSecretBySlug(userId: string, slug: string, masterPassword?: string): Promise {
-    const user = await this.getUserWithCompany(userId);
-
-    const secret = await this.secretRepository.findOne({
-      where: {
-        slug,
-        companyId: user.company.id,
-      },
-    });
-
-    if (!secret) {
-      throw new NotFoundException('Secret not found');
-    }
-
-    // Check expiration
-    if (secret.expiresAt && secret.expiresAt < new Date()) {
-      throw new GoneException('Secret has expired');
-    }
-
-    // Check master password if required
-    if (secret.masterEncryption && !masterPassword) {
-      throw new ForbiddenException('Master password required');
-    }
-
-    if (secret.masterEncryption && masterPassword) {
-      // Verify master password against stored hash
-      const isValid = await Encryptor.verifyUserPassword(masterPassword, secret.masterHash);
-      if (!isValid) {
-        await this.logAccess(secret.id, userId, SecretActionEnum.VIEW, false, 'Invalid master password');
-        throw new ForbiddenException('Invalid master password');
-      }
-    }
-
-    // Update last accessed
-    secret.lastAccessedAt = new Date();
-    await this.secretRepository.save(secret);
-
-    // Log access
-    await this.logAccess(secret.id, userId, SecretActionEnum.VIEW);
-
-    return this.buildFoundSecretDto(secret, true, masterPassword);
-  }
-
-  async updateSecret(
-    userId: string,
-    slug: string,
-    updateDto: UpdateSecretDto,
-    masterPassword?: string,
-  ): Promise {
-    const user = await this.getUserWithCompany(userId);
-
-    const secret = await this.secretRepository.findOne({
-      where: {
-        slug,
-        companyId: user.company.id,
-      },
-    });
-
-    if (!secret) {
-      throw new NotFoundException('Secret not found');
-    }
-
-    // Check expiration
-    if (secret.expiresAt && secret.expiresAt < new Date()) {
-      throw new GoneException('Secret has expired');
-    }
-
-    // Check master password if required
-    if (secret.masterEncryption && !masterPassword) {
-      throw new ForbiddenException('Master password required');
-    }
-
-    // Verify master password if required
-    if (secret.masterEncryption && masterPassword) {
-      const isValid = await Encryptor.verifyUserPassword(masterPassword, secret.masterHash);
-      if (!isValid) {
-        await this.logAccess(secret.id, userId, SecretActionEnum.UPDATE, false, 'Invalid master password');
-        throw new ForbiddenException('Invalid master password');
-      }
-    }
-
-    // Encrypt the updated secret value if provided
-    if (updateDto.value) {
-      let encryptedValue = updateDto.value;
-
-      // Layer 2: Encrypt with master password first (if enabled)
-      if (secret.masterEncryption && masterPassword) {
-        encryptedValue = Encryptor.encryptDataMasterPwd(encryptedValue, masterPassword);
-      }
-
-      // Layer 1: Encrypt with PRIVATE_KEY (always)
-      encryptedValue = Encryptor.encryptData(encryptedValue);
-
-      secret.encryptedValue = encryptedValue;
-    }
-
-    if (updateDto.expiresAt !== undefined) {
-      secret.expiresAt = updateDto.expiresAt ? new Date(updateDto.expiresAt) : null;
-    }
-
-    const updated = await this.secretRepository.save(secret);
-
-    // Log update
-    await this.logAccess(secret.id, userId, SecretActionEnum.UPDATE);
-
-    return this.buildFoundSecretDto(updated, false);
-  }
-
-  async deleteSecret(userId: string, slug: string): Promise<{ message: string; deletedAt: Date }> {
-    const user = await this.getUserWithCompany(userId);
-
-    const secret = await this.secretRepository.findOne({
-      where: {
-        slug,
-        companyId: user.company.id,
-      },
-    });
-
-    if (!secret) {
-      throw new NotFoundException('Secret not found');
-    }
-
-    // Log deletion before deleting
-    await this.logAccess(secret.id, userId, SecretActionEnum.DELETE);
-
-    // Permanently delete
-    await this.secretRepository.remove(secret);
-
-    return {
-      message: 'Secret deleted successfully',
-      deletedAt: new Date(),
-    };
-  }
-
-  async getAuditLog(
-    userId: string,
-    slug: string,
-    options: { page: number; limit: number },
-  ): Promise {
-    const user = await this.getUserWithCompany(userId);
-
-    const secret = await this.secretRepository.findOne({
-      where: {
-        slug,
-        companyId: user.company.id,
-      },
-    });
-
-    if (!secret) {
-      throw new NotFoundException('Secret not found');
-    }
-
-    const [logs, total] = await this.auditLogRepository.findAndCount({
-      where: {
-        secretId: secret.id,
-      },
-      relations: ['user'],
-      order: {
-        accessedAt: 'DESC',
-      },
-      skip: (options.page - 1) * options.limit,
-      take: options.limit,
-    });
-
-    return {
-      data: logs.map((log) => this.buildAuditLogEntryDto(log)),
-      pagination: {
-        total,
-        page: options.page,
-        limit: options.limit,
-        totalPages: Math.ceil(total / options.limit),
-      },
-    };
-  }
-
-  private buildFoundSecretDto(
-    secret: UserSecretEntity,
-    includeValue: boolean,
-    masterPassword?: string,
-  ): FoundSecretDto {
-    let decryptedValue: string | undefined = undefined;
-
-    if (includeValue) {
-      // Layer 1: Decrypt with PRIVATE_KEY (always)
-      let value = Encryptor.decryptData(secret.encryptedValue);
-
-      // Layer 2: Decrypt with master password (if enabled)
-      if (secret.masterEncryption && masterPassword) {
-        value = Encryptor.decryptDataMasterPwd(value, masterPassword);
-      }
-
-      decryptedValue = value;
-    }
-
-    return {
-      id: secret.id,
-      slug: secret.slug,
-      value: decryptedValue,
-      companyId: secret.companyId,
-      createdAt: secret.createdAt,
-      updatedAt: secret.updatedAt,
-      lastAccessedAt: secret.lastAccessedAt,
-      expiresAt: secret.expiresAt,
-      masterEncryption: secret.masterEncryption,
-    };
-  }
-
-  private buildSecretListItemDto(secret: UserSecretEntity): SecretListItemDto {
-    return {
-      id: secret.id,
-      slug: secret.slug,
-      companyId: secret.companyId,
-      createdAt: secret.createdAt,
-      updatedAt: secret.updatedAt,
-      lastAccessedAt: secret.lastAccessedAt,
-      expiresAt: secret.expiresAt,
-      masterEncryption: secret.masterEncryption,
-    };
-  }
-
-  private buildAuditLogEntryDto(log: SecretAccessLogEntity): AuditLogEntryDto {
-    return {
-      id: log.id,
-      action: log.action,
-      user: {
-        id: log.user.id,
-        email: log.user.email,
-      },
-      accessedAt: log.accessedAt,
-      ipAddress: log.ipAddress,
-      userAgent: log.userAgent,
-      success: log.success,
-      errorMessage: log.errorMessage,
-    };
-  }
-}
diff --git a/backend/src/entities/user-secret/utils/build-audit-log-entry.ds.ts b/backend/src/entities/user-secret/utils/build-audit-log-entry.ds.ts
new file mode 100644
index 000000000..8112f8d27
--- /dev/null
+++ b/backend/src/entities/user-secret/utils/build-audit-log-entry.ds.ts
@@ -0,0 +1,18 @@
+import { SecretAccessLogEntity } from '../../secret-access-log/secret-access-log.entity.js';
+import { AuditLogEntryDS } from '../application/data-structures/get-audit-log.ds.js';
+
+export function buildAuditLogEntryDS(log: SecretAccessLogEntity): AuditLogEntryDS {
+  return {
+    id: log.id,
+    action: log.action,
+    user: {
+      id: log.user.id,
+      email: log.user.email,
+    },
+    accessedAt: log.accessedAt,
+    ipAddress: log.ipAddress,
+    userAgent: log.userAgent,
+    success: log.success,
+    errorMessage: log.errorMessage,
+  };
+}
diff --git a/backend/src/entities/user-secret/utils/build-audit-log.dto.ts b/backend/src/entities/user-secret/utils/build-audit-log.dto.ts
new file mode 100644
index 000000000..67a31561f
--- /dev/null
+++ b/backend/src/entities/user-secret/utils/build-audit-log.dto.ts
@@ -0,0 +1,22 @@
+import { AuditLogResponseDto, AuditLogEntryDto } from '../application/dto/audit-log.dto.js';
+import { AuditLogListDS, AuditLogEntryDS } from '../application/data-structures/get-audit-log.ds.js';
+
+export function buildAuditLogEntryDto(ds: AuditLogEntryDS): AuditLogEntryDto {
+  return {
+    id: ds.id,
+    action: ds.action,
+    user: ds.user,
+    accessedAt: ds.accessedAt,
+    ipAddress: ds.ipAddress,
+    userAgent: ds.userAgent,
+    success: ds.success,
+    errorMessage: ds.errorMessage,
+  };
+}
+
+export function buildAuditLogResponseDto(ds: AuditLogListDS): AuditLogResponseDto {
+  return {
+    data: ds.data.map(buildAuditLogEntryDto),
+    pagination: ds.pagination,
+  };
+}
diff --git a/backend/src/entities/user-secret/utils/build-created-secret.ds.ts b/backend/src/entities/user-secret/utils/build-created-secret.ds.ts
new file mode 100644
index 000000000..9a55c4d45
--- /dev/null
+++ b/backend/src/entities/user-secret/utils/build-created-secret.ds.ts
@@ -0,0 +1,15 @@
+import { CreatedSecretDS } from '../application/data-structures/created-secret.ds.js';
+import { UserSecretEntity } from '../user-secret.entity.js';
+
+export function buildCreatedSecretDS(secret: UserSecretEntity): CreatedSecretDS {
+  return {
+    id: secret.id,
+    slug: secret.slug,
+    companyId: secret.companyId,
+    createdAt: secret.createdAt,
+    updatedAt: secret.updatedAt,
+    lastAccessedAt: secret.lastAccessedAt,
+    expiresAt: secret.expiresAt,
+    masterEncryption: secret.masterEncryption,
+  };
+}
diff --git a/backend/src/entities/user-secret/utils/build-created-secret.dto.ts b/backend/src/entities/user-secret/utils/build-created-secret.dto.ts
new file mode 100644
index 000000000..fdfbdc9db
--- /dev/null
+++ b/backend/src/entities/user-secret/utils/build-created-secret.dto.ts
@@ -0,0 +1,16 @@
+import { FoundSecretDto } from '../application/dto/found-secret.dto.js';
+import { CreatedSecretDS } from '../application/data-structures/created-secret.ds.js';
+
+export function buildCreatedSecretDto(ds: CreatedSecretDS): FoundSecretDto {
+  return {
+    id: ds.id,
+    slug: ds.slug,
+    value: undefined,
+    companyId: ds.companyId,
+    createdAt: ds.createdAt,
+    updatedAt: ds.updatedAt,
+    lastAccessedAt: ds.lastAccessedAt,
+    expiresAt: ds.expiresAt,
+    masterEncryption: ds.masterEncryption,
+  };
+}
diff --git a/backend/src/entities/user-secret/utils/build-found-secret.ds.ts b/backend/src/entities/user-secret/utils/build-found-secret.ds.ts
new file mode 100644
index 000000000..898800d64
--- /dev/null
+++ b/backend/src/entities/user-secret/utils/build-found-secret.ds.ts
@@ -0,0 +1,16 @@
+import { FoundSecretDS } from '../application/data-structures/found-secret.ds.js';
+import { UserSecretEntity } from '../user-secret.entity.js';
+
+export function buildFoundSecretDS(secret: UserSecretEntity, decryptedValue?: string): FoundSecretDS {
+  return {
+    id: secret.id,
+    slug: secret.slug,
+    value: decryptedValue,
+    companyId: secret.companyId,
+    createdAt: secret.createdAt,
+    updatedAt: secret.updatedAt,
+    lastAccessedAt: secret.lastAccessedAt,
+    expiresAt: secret.expiresAt,
+    masterEncryption: secret.masterEncryption,
+  };
+}
diff --git a/backend/src/entities/user-secret/utils/build-found-secret.dto.ts b/backend/src/entities/user-secret/utils/build-found-secret.dto.ts
new file mode 100644
index 000000000..81504154f
--- /dev/null
+++ b/backend/src/entities/user-secret/utils/build-found-secret.dto.ts
@@ -0,0 +1,16 @@
+import { FoundSecretDto } from '../application/dto/found-secret.dto.js';
+import { FoundSecretDS } from '../application/data-structures/found-secret.ds.js';
+
+export function buildFoundSecretDto(ds: FoundSecretDS): FoundSecretDto {
+  return {
+    id: ds.id,
+    slug: ds.slug,
+    value: ds.value,
+    companyId: ds.companyId,
+    createdAt: ds.createdAt,
+    updatedAt: ds.updatedAt,
+    lastAccessedAt: ds.lastAccessedAt,
+    expiresAt: ds.expiresAt,
+    masterEncryption: ds.masterEncryption,
+  };
+}
diff --git a/backend/src/entities/user-secret/utils/build-secret-list-item.ds.ts b/backend/src/entities/user-secret/utils/build-secret-list-item.ds.ts
new file mode 100644
index 000000000..37b63eb9a
--- /dev/null
+++ b/backend/src/entities/user-secret/utils/build-secret-list-item.ds.ts
@@ -0,0 +1,15 @@
+import { SecretListItemDS } from '../application/data-structures/get-secrets.ds.js';
+import { UserSecretEntity } from '../user-secret.entity.js';
+
+export function buildSecretListItemDS(secret: UserSecretEntity): SecretListItemDS {
+  return {
+    id: secret.id,
+    slug: secret.slug,
+    companyId: secret.companyId,
+    createdAt: secret.createdAt,
+    updatedAt: secret.updatedAt,
+    lastAccessedAt: secret.lastAccessedAt,
+    expiresAt: secret.expiresAt,
+    masterEncryption: secret.masterEncryption,
+  };
+}
diff --git a/backend/src/entities/user-secret/utils/build-secret-list.dto.ts b/backend/src/entities/user-secret/utils/build-secret-list.dto.ts
new file mode 100644
index 000000000..add45f480
--- /dev/null
+++ b/backend/src/entities/user-secret/utils/build-secret-list.dto.ts
@@ -0,0 +1,22 @@
+import { SecretListResponseDto, SecretListItemDto } from '../application/dto/secret-list.dto.js';
+import { SecretsListDS, SecretListItemDS } from '../application/data-structures/get-secrets.ds.js';
+
+export function buildSecretListItemDto(ds: SecretListItemDS): SecretListItemDto {
+  return {
+    id: ds.id,
+    slug: ds.slug,
+    companyId: ds.companyId,
+    createdAt: ds.createdAt,
+    updatedAt: ds.updatedAt,
+    lastAccessedAt: ds.lastAccessedAt,
+    expiresAt: ds.expiresAt,
+    masterEncryption: ds.masterEncryption,
+  };
+}
+
+export function buildSecretListResponseDto(ds: SecretsListDS): SecretListResponseDto {
+  return {
+    data: ds.data.map(buildSecretListItemDto),
+    pagination: ds.pagination,
+  };
+}
diff --git a/backend/src/entities/user-secret/utils/build-updated-secret.ds.ts b/backend/src/entities/user-secret/utils/build-updated-secret.ds.ts
new file mode 100644
index 000000000..d42b19321
--- /dev/null
+++ b/backend/src/entities/user-secret/utils/build-updated-secret.ds.ts
@@ -0,0 +1,15 @@
+import { UpdatedSecretDS } from '../application/data-structures/update-secret.ds.js';
+import { UserSecretEntity } from '../user-secret.entity.js';
+
+export function buildUpdatedSecretDS(secret: UserSecretEntity): UpdatedSecretDS {
+  return {
+    id: secret.id,
+    slug: secret.slug,
+    companyId: secret.companyId,
+    createdAt: secret.createdAt,
+    updatedAt: secret.updatedAt,
+    lastAccessedAt: secret.lastAccessedAt,
+    expiresAt: secret.expiresAt,
+    masterEncryption: secret.masterEncryption,
+  };
+}
diff --git a/backend/src/entities/user-secret/utils/build-updated-secret.dto.ts b/backend/src/entities/user-secret/utils/build-updated-secret.dto.ts
new file mode 100644
index 000000000..3fd9dce0c
--- /dev/null
+++ b/backend/src/entities/user-secret/utils/build-updated-secret.dto.ts
@@ -0,0 +1,16 @@
+import { FoundSecretDto } from '../application/dto/found-secret.dto.js';
+import { UpdatedSecretDS } from '../application/data-structures/update-secret.ds.js';
+
+export function buildUpdatedSecretDto(ds: UpdatedSecretDS): FoundSecretDto {
+  return {
+    id: ds.id,
+    slug: ds.slug,
+    value: undefined,
+    companyId: ds.companyId,
+    createdAt: ds.createdAt,
+    updatedAt: ds.updatedAt,
+    lastAccessedAt: ds.lastAccessedAt,
+    expiresAt: ds.expiresAt,
+    masterEncryption: ds.masterEncryption,
+  };
+}

From 8109d6c2bff89750a5f8af6c9923946f0865cf17 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Fri, 28 Nov 2025 13:04:40 +0000
Subject: [PATCH 05/23] fix test runner

---
 .github/workflows/backend.yml                          | 10 ++++------
 .../repository/user-secret-repository.extension.ts     | 10 ++++------
 .../repository/user-secret-repository.interface.ts     |  8 +++++++-
 docker-compose.tst.yml                                 |  2 +-
 4 files changed, 16 insertions(+), 14 deletions(-)

diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
index 158411246..9e3461b17 100644
--- a/.github/workflows/backend.yml
+++ b/.github/workflows/backend.yml
@@ -3,11 +3,11 @@ on:
   push:
     branches:
       - main
-    paths:
-      - "!frontend/**"
+    paths-ignore:
+      - "frontend/**"
   pull_request:
-    paths: 
-      - "!frontend/**"
+    paths-ignore:
+      - "frontend/**"
 jobs:
   test:
     runs-on:
@@ -40,5 +40,3 @@ jobs:
           node-version: '18'
       - run: cd backend && yarn install
       - run: cd backend && yarn run lint
-
-      
diff --git a/backend/src/entities/user-secret/repository/user-secret-repository.extension.ts b/backend/src/entities/user-secret/repository/user-secret-repository.extension.ts
index c1a03bb52..9ac42f78c 100644
--- a/backend/src/entities/user-secret/repository/user-secret-repository.extension.ts
+++ b/backend/src/entities/user-secret/repository/user-secret-repository.extension.ts
@@ -1,11 +1,10 @@
-import { Like } from 'typeorm';
+import { FindOptionsWhere, Like } from 'typeorm';
 import { UserSecretEntity } from '../user-secret.entity.js';
 import { IUserSecretRepository } from './user-secret-repository.interface.js';
 
 export const userSecretRepositoryExtension: IUserSecretRepository = {
   async findSecretBySlugAndCompanyId(slug: string, companyId: string): Promise {
-    const self = this as any;
-    return self.findOne({
+    return this.findOne({
       where: {
         slug,
         companyId,
@@ -17,8 +16,7 @@ export const userSecretRepositoryExtension: IUserSecretRepository = {
     companyId: string,
     options: { page: number; limit: number; search?: string },
   ): Promise<[UserSecretEntity[], number]> {
-    const self = this as any;
-    const where: any = {
+    const where: FindOptionsWhere = {
       companyId,
     };
 
@@ -26,7 +24,7 @@ export const userSecretRepositoryExtension: IUserSecretRepository = {
       where.slug = Like(`%${options.search}%`);
     }
 
-    return self.findAndCount({
+    return this.findAndCount({
       where,
       order: {
         createdAt: 'DESC',
diff --git a/backend/src/entities/user-secret/repository/user-secret-repository.interface.ts b/backend/src/entities/user-secret/repository/user-secret-repository.interface.ts
index 5e5089818..8040435c7 100644
--- a/backend/src/entities/user-secret/repository/user-secret-repository.interface.ts
+++ b/backend/src/entities/user-secret/repository/user-secret-repository.interface.ts
@@ -1,8 +1,14 @@
+import { Repository } from 'typeorm';
 import { UserSecretEntity } from '../user-secret.entity.js';
 
 export interface IUserSecretRepository {
-  findSecretBySlugAndCompanyId(slug: string, companyId: string): Promise;
+  findSecretBySlugAndCompanyId(
+    this: Repository,
+    slug: string,
+    companyId: string,
+  ): Promise;
   findSecretsForCompany(
+    this: Repository,
     companyId: string,
     options: { page: number; limit: number; search?: string },
   ): Promise<[UserSecretEntity[], number]>;
diff --git a/docker-compose.tst.yml b/docker-compose.tst.yml
index 87e78c4ec..c7e1511bb 100644
--- a/docker-compose.tst.yml
+++ b/docker-compose.tst.yml
@@ -54,7 +54,7 @@ services:
       - test-ibm-db2-e2e-testing
       - test-mongo-e2e-testing
       - test-dynamodb-e2e-testing
-    command: ["/bin/sh", "-c", "yarn test-all $EXTRA_ARGS"]
+    command: ["/bin/sh", "-c", "yarn test $EXTRA_ARGS"]
     develop:
       watch:
         - action: rebuild

From 55589aa01e4b6b854804121e82529b896d3d73f1 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Fri, 28 Nov 2025 13:22:35 +0000
Subject: [PATCH 06/23] errors to constants

---
 .../user-secret/use-cases/create-secret.use.case.ts |  5 +++--
 .../user-secret/use-cases/delete-secret.use.case.ts |  7 ++++---
 .../use-cases/get-secret-audit-log.use.case.ts      |  5 +++--
 .../use-cases/get-secret-by-slug.use.case.ts        | 13 +++++++------
 .../user-secret/use-cases/get-secrets.use.case.ts   |  3 ++-
 .../user-secret/use-cases/update-secret.use.case.ts | 13 +++++++------
 backend/src/exceptions/text/messages.ts             |  7 +++++++
 7 files changed, 33 insertions(+), 20 deletions(-)

diff --git a/backend/src/entities/user-secret/use-cases/create-secret.use.case.ts b/backend/src/entities/user-secret/use-cases/create-secret.use.case.ts
index 556260e04..94b63a7ea 100644
--- a/backend/src/entities/user-secret/use-cases/create-secret.use.case.ts
+++ b/backend/src/entities/user-secret/use-cases/create-secret.use.case.ts
@@ -8,6 +8,7 @@ import { ICreateSecret } from './user-secret-use-cases.interface.js';
 import { buildCreatedSecretDS } from '../utils/build-created-secret.ds.js';
 import { Encryptor } from '../../../helpers/encryption/encryptor.js';
 import { SecretActionEnum } from '../../secret-access-log/secret-access-log.entity.js';
+import { Messages } from '../../../exceptions/text/messages.js';
 
 @Injectable({ scope: Scope.REQUEST })
 export class CreateSecretUseCase extends AbstractUseCase implements ICreateSecret {
@@ -27,13 +28,13 @@ export class CreateSecretUseCase extends AbstractUseCase implements IDeleteSecret {
@@ -24,13 +25,13 @@ export class DeleteSecretUseCase extends AbstractUseCase implements IGetSecretBySlug {
@@ -27,21 +28,21 @@ export class GetSecretBySlugUseCase extends AbstractUseCase implements IGetSecrets {
@@ -24,7 +25,7 @@ export class GetSecretsUseCase extends AbstractUseCase implements IUpdateSecret {
@@ -26,21 +27,21 @@ export class UpdateSecretUseCase extends AbstractUseCase
Date: Fri, 28 Nov 2025 14:11:45 +0000
Subject: [PATCH 07/23] wait for health checks

---
 docker-compose.tst.yml | 83 +++++++++++++++++++++++++++++++++---------
 1 file changed, 66 insertions(+), 17 deletions(-)

diff --git a/docker-compose.tst.yml b/docker-compose.tst.yml
index c7e1511bb..b6c0fbf1f 100644
--- a/docker-compose.tst.yml
+++ b/docker-compose.tst.yml
@@ -37,23 +37,22 @@ services:
     volumes:
       - ./backend/src/migrations:/app/src/migrations
     depends_on:
-      - postgres
-      - testMySQL-e2e-testing
-      - testPg-e2e-testing
-      - mssql-e2e-testing
-      - test-oracle-e2e-testing
-      - test-ibm-db2-e2e-testing
-      - test-mongo-e2e-testing
-      - test-dynamodb-e2e-testing
-    links:
-      - postgres
-      - testMySQL-e2e-testing
-      - testPg-e2e-testing
-      - mssql-e2e-testing
-      - test-oracle-e2e-testing
-      - test-ibm-db2-e2e-testing
-      - test-mongo-e2e-testing
-      - test-dynamodb-e2e-testing
+      postgres:
+        condition: service_healthy
+      testMySQL-e2e-testing:
+        condition: service_healthy
+      testPg-e2e-testing:
+        condition: service_healthy
+      mssql-e2e-testing:
+        condition: service_healthy
+      test-oracle-e2e-testing:
+        condition: service_healthy
+      test-ibm-db2-e2e-testing:
+        condition: service_healthy
+      test-mongo-e2e-testing:
+        condition: service_healthy
+      test-dynamodb-e2e-testing:
+        condition: service_healthy
     command: ["/bin/sh", "-c", "yarn test $EXTRA_ARGS"]
     develop:
       watch:
@@ -69,6 +68,12 @@ services:
     environment:
       MYSQL_ROOT_PASSWORD: 123
       MYSQL_DATABASE: testDB
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p123"]
+      interval: 10s
+      timeout: 5s
+      retries: 10
+      start_period: 30s
 
   testPg-e2e-testing:
     image: postgres
@@ -77,6 +82,12 @@ services:
     environment:
       POSTGRES_PASSWORD: 123
     command: postgres -c 'max_connections=300'
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U postgres"]
+      interval: 10s
+      timeout: 5s
+      retries: 10
+      start_period: 10s
 
   test-mongo-e2e-testing:
     image: mongo
@@ -86,6 +97,12 @@ services:
       MONGO_INITDB_ROOT_PASSWORD: example
     ports:
       - 27017:27017
+    healthcheck:
+      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
+      interval: 10s
+      timeout: 5s
+      retries: 10
+      start_period: 30s
 
   postgres:
     image: postgres
@@ -96,6 +113,12 @@ services:
     command: postgres -c 'max_connections=300'
     tmpfs:
       - /var/lib/postgresql
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U postgres"]
+      interval: 10s
+      timeout: 5s
+      retries: 10
+      start_period: 10s
 
   mssql-e2e-testing:
     image: mcr.microsoft.com/mssql/server:2019-latest
@@ -104,6 +127,12 @@ services:
       - ACCEPT_EULA=Y
     ports:
       - "5434:1433"
+    healthcheck:
+      test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'yNuXf@6T#BgoQ%U6knMp' -C -Q 'SELECT 1' || /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'yNuXf@6T#BgoQ%U6knMp' -Q 'SELECT 1'"]
+      interval: 10s
+      timeout: 5s
+      retries: 20
+      start_period: 60s
 
   test-oracle-e2e-testing:
     image: gvenzl/oracle-xe
@@ -111,6 +140,12 @@ services:
       - 1521:1521
     environment:
       ORACLE_PASSWORD: 12345
+    healthcheck:
+      test: ["CMD-SHELL", "healthcheck.sh"]
+      interval: 10s
+      timeout: 5s
+      retries: 30
+      start_period: 120s
 
   autoadmin-ws-server:
     build:
@@ -239,9 +274,23 @@ services:
       - ETCD_PASSWORD=
     ports:
       - 50000:50000
+    healthcheck:
+      test: ["CMD-SHELL", "su - db2inst1 -c 'db2 connect to testdb'"]
+      interval: 30s
+      timeout: 30s
+      retries: 20
+      start_period: 180s
 
   test-dynamodb-e2e-testing:
     image: amazon/dynamodb-local
     environment:
       - AWS_ACCESS_KEY_ID=SuperSecretAwsAccessKey
       - AWS_SECRET=SuperSecretAwsSecret
+    ports:
+      - 8000:8000
+    healthcheck:
+      test: ["CMD-SHELL", "curl -s http://localhost:8000 || exit 1"]
+      interval: 10s
+      timeout: 5s
+      retries: 10
+      start_period: 10s

From 9f3b1d416e2e1c7fc05fffe504bae57d66eb00b1 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Fri, 28 Nov 2025 15:28:37 +0000
Subject: [PATCH 08/23] fix redis tests

---
 docker-compose.tst.yml | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/docker-compose.tst.yml b/docker-compose.tst.yml
index b6c0fbf1f..a469e0a53 100644
--- a/docker-compose.tst.yml
+++ b/docker-compose.tst.yml
@@ -1,4 +1,3 @@
-version: "3.9"
 services:
   backend:
     build:
@@ -53,6 +52,8 @@ services:
         condition: service_healthy
       test-dynamodb-e2e-testing:
         condition: service_healthy
+      test-redis-e2e-testing:
+        condition: service_healthy
     command: ["/bin/sh", "-c", "yarn test $EXTRA_ARGS"]
     develop:
       watch:
@@ -161,6 +162,15 @@ services:
     depends_on:
       - backend
 
+  test-redis-e2e-testing:
+    image: redis:7.0.11
+    command: ["redis-server", "--requirepass", "SuperSecretRedisPassword"]
+    healthcheck:
+      test: ["CMD", "redis-cli", "ping"]
+      interval: 30s
+      timeout: 10s
+      retries: 3
+
   # rocketadmin-agent_oracle:
   #   build:
   #     context: .

From b40d3d6355d65954cb16902c0ea5c6b99f64828d Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Fri, 28 Nov 2025 15:59:01 +0000
Subject: [PATCH 09/23] fix cassandra and redis tests

---
 backend/test/mock.factory.ts |  2 +-
 docker-compose.tst.yml       | 31 +++++++++++++++++++++++++++++++
 2 files changed, 32 insertions(+), 1 deletion(-)

diff --git a/backend/test/mock.factory.ts b/backend/test/mock.factory.ts
index b718332c7..42a8f3495 100644
--- a/backend/test/mock.factory.ts
+++ b/backend/test/mock.factory.ts
@@ -294,7 +294,7 @@ export class MockFactory {
     const dto = new CreateConnectionDto() as any;
     dto.title = 'Test connection to Redis in Docker';
     dto.type = ConnectionTypesEnum.redis;
-    dto.host = 'redis-e2e-testing';
+    dto.host = 'test-redis-e2e-testing';
     dto.port = 6379;
     dto.password = 'SuperSecretRedisPassword';
     dto.ssh = false;
diff --git a/docker-compose.tst.yml b/docker-compose.tst.yml
index a469e0a53..ffd0268f2 100644
--- a/docker-compose.tst.yml
+++ b/docker-compose.tst.yml
@@ -17,6 +17,8 @@ services:
       - test-oracle-e2e-testing
       - test-ibm-db2-e2e-testing
       - test-dynamodb-e2e-testing
+      - test-redis-e2e-testing
+      - test-cassandra-e2e-testing
     links:
       - postgres
       - testMySQL-e2e-testing
@@ -25,6 +27,8 @@ services:
       - test-oracle-e2e-testing
       - test-ibm-db2-e2e-testing
       - test-dynamodb-e2e-testing
+      - test-redis-e2e-testing
+      - test-cassandra-e2e-testing
     command: ["yarn", "start"]
   backend_test:
     build:
@@ -54,6 +58,8 @@ services:
         condition: service_healthy
       test-redis-e2e-testing:
         condition: service_healthy
+      test-cassandra-e2e-testing:
+        condition: service_healthy
     command: ["/bin/sh", "-c", "yarn test $EXTRA_ARGS"]
     develop:
       watch:
@@ -161,6 +167,31 @@ services:
       - backend
     depends_on:
       - backend
+  test-cassandra-e2e-testing:
+    image: cassandra:5.0.4
+    ports:
+      - 9042:9042
+    environment:
+      - CASSANDRA_CLUSTER_NAME=TestCluster
+      - CASSANDRA_DC=TestDC
+      - CASSANDRA_RACK=TestRack
+    restart: always
+    healthcheck:
+      test:
+        [
+          "CMD",
+          "cqlsh",
+          "-u",
+          "cassandra",
+          "-p",
+          "cassandra",
+          "-e",
+          "describe keyspaces",
+        ]
+      interval: 30s
+      timeout: 10s
+      retries: 5
+      start_period: 60s
 
   test-redis-e2e-testing:
     image: redis:7.0.11

From 63b2db5947aaf1972cb08e809dfb74fafa23d9a2 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Fri, 28 Nov 2025 17:53:53 +0000
Subject: [PATCH 10/23] bigger instance

---
 .github/workflows/backend.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
index 9e3461b17..3c1a994a2 100644
--- a/.github/workflows/backend.yml
+++ b/.github/workflows/backend.yml
@@ -11,7 +11,7 @@ on:
 jobs:
   test:
     runs-on:
-      labels: ubuntu-latest-4-cores
+      labels: ubuntu-latest-8-cores
     steps:
       - uses: actions/checkout@v3
       - uses: extractions/setup-just@v1

From 4be6e2e695641f66fea81cc4e342cadb5e2891e3 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Sat, 29 Nov 2025 10:04:33 +0000
Subject: [PATCH 11/23] fix tests to handle readonly file system

---
 .../non-saas-table-cassandra.e2e.test.ts      | 26 +++++--------------
 .../non-saas-table-ibmdb2-e2e.test.ts         | 13 +++-------
 .../non-saas-table-mongodb-e2e.test.ts        | 13 +++-------
 .../non-saas-table-redis-e2e.test.ts          | 19 +++-----------
 4 files changed, 16 insertions(+), 55 deletions(-)

diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-cassandra.e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-cassandra.e2e.test.ts
index 5ac8c76e1..0f6626dbe 100644
--- a/backend/test/ava-tests/non-saas-tests/non-saas-table-cassandra.e2e.test.ts
+++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-cassandra.e2e.test.ts
@@ -27,6 +27,7 @@ import { registerUserAndReturnUserInfo } from '../../utils/register-user-and-ret
 import { TestUtils } from '../../utils/test.utils.js';
 import { setSaasEnvVariable } from '../../utils/set-saas-env-variable.js';
 import { WinstonLogger } from '../../../src/entities/logging/winston-logger.js';
+import os from "node:os";
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
@@ -3461,12 +3462,8 @@ test.serial(`${currentTest} should return csv file with table data`, async (t) =
   }
   t.is(getTableCsvResponse.status, 201);
   const fileName = `${testTableName}.csv`;
-  const downloadedFilePatch = join(__dirname, 'response-files', fileName);
+  const downloadedFilePatch = join(os.tmpdir(), fileName);
 
-  const dir = join(__dirname, 'response-files');
-  if (!fs.existsSync(dir)) {
-    fs.mkdirSync(dir);
-  }
   // eslint-disable-next-line security/detect-non-literal-fs-filename
   fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body);
 
@@ -3584,12 +3581,9 @@ with search and pagination: page=1, perPage=2 and DESC sorting`,
     }
     t.is(getTableCsvResponse.status, 201);
     const fileName = `${testTableName}.csv`;
-    const downloadedFilePatch = join(__dirname, 'response-files', fileName);
+    const tmpDir = os.tmpdir();
+    const downloadedFilePatch = join(tmpDir, fileName);
 
-    const dir = join(__dirname, 'response-files');
-    if (!fs.existsSync(dir)) {
-      fs.mkdirSync(dir);
-    }
     // eslint-disable-next-line security/detect-non-literal-fs-filename
     fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body);
 
@@ -3653,12 +3647,8 @@ test.serial(`${currentTest} should import csv file with table data`, async (t) =
   }
   t.is(getTableCsvResponse.status, 201);
   const fileName = `${testTableName}.csv`;
-  const downloadedFilePatch = join(__dirname, 'response-files', fileName);
+  const downloadedFilePatch = join(os.tmpdir(), fileName);
 
-  const dir = join(__dirname, 'response-files');
-  if (!fs.existsSync(dir)) {
-    fs.mkdirSync(dir);
-  }
   // eslint-disable-next-line security/detect-non-literal-fs-filename
   fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body);
   // eslint-disable-next-line security/detect-non-literal-fs-filename
@@ -3762,12 +3752,8 @@ test.serial(`${currentTest} should throw exception whe csv import is disabled`,
   }
   t.is(getTableCsvResponse.status, 201);
   const fileName = `${testTableName}.csv`;
-  const downloadedFilePatch = join(__dirname, 'response-files', fileName);
+  const downloadedFilePatch = join(os.tmpdir(), fileName);
 
-  const dir = join(__dirname, 'response-files');
-  if (!fs.existsSync(dir)) {
-    fs.mkdirSync(dir);
-  }
   // eslint-disable-next-line security/detect-non-literal-fs-filename
   fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body);
   // eslint-disable-next-line security/detect-non-literal-fs-filename
diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-ibmdb2-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-ibmdb2-e2e.test.ts
index f5d83979a..d084d3f2e 100644
--- a/backend/test/ava-tests/non-saas-tests/non-saas-table-ibmdb2-e2e.test.ts
+++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-ibmdb2-e2e.test.ts
@@ -28,6 +28,7 @@ import { fileURLToPath } from 'url';
 import { join } from 'path';
 import { setSaasEnvVariable } from '../../utils/set-saas-env-variable.js';
 import { WinstonLogger } from '../../../src/entities/logging/winston-logger.js';
+import os from 'node:os';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
@@ -3409,12 +3410,8 @@ test.serial(`${currentTest} should return csv file with table data`, async (t) =
   }
   t.is(getTableCsvResponse.status, 201);
   const fileName = `${testTableName}.csv`;
-  const downloadedFilePatch = join(__dirname, 'response-files', fileName);
+  const downloadedFilePatch = join(os.tmpdir(), fileName);
 
-  const dir = join(__dirname, 'response-files');
-  if (!fs.existsSync(dir)) {
-    fs.mkdirSync(dir);
-  }
   // eslint-disable-next-line security/detect-non-literal-fs-filename
   fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body);
 
@@ -3479,12 +3476,8 @@ with search and pagination: page=1, perPage=2 and DESC sorting`,
     }
     t.is(getTableCsvResponse.status, 201);
     const fileName = `${testTableName}.csv`;
-    const downloadedFilePatch = join(__dirname, 'response-files', fileName);
+    const downloadedFilePatch = join(os.tmpdir(), fileName);
 
-    const dir = join(__dirname, 'response-files');
-    if (!fs.existsSync(dir)) {
-      fs.mkdirSync(dir);
-    }
     // eslint-disable-next-line security/detect-non-literal-fs-filename
     fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body);
 
diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-mongodb-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-mongodb-e2e.test.ts
index efed56704..e733085f5 100644
--- a/backend/test/ava-tests/non-saas-tests/non-saas-table-mongodb-e2e.test.ts
+++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-mongodb-e2e.test.ts
@@ -28,6 +28,7 @@ import { send } from 'process';
 import { setSaasEnvVariable } from '../../utils/set-saas-env-variable.js';
 import { Cacher } from '../../../src/helpers/cache/cacher.js';
 import { WinstonLogger } from '../../../src/entities/logging/winston-logger.js';
+import os from 'node:os';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
@@ -3517,12 +3518,8 @@ test.serial(`${currentTest} should return csv file with table data`, async (t) =
   }
   t.is(getTableCsvResponse.status, 201);
   const fileName = `${testTableName}.csv`;
-  const downloadedFilePatch = join(__dirname, 'response-files', fileName);
+  const downloadedFilePatch = join(os.tmpdir(), fileName);
 
-  const dir = join(__dirname, 'response-files');
-  if (!fs.existsSync(dir)) {
-    fs.mkdirSync(dir);
-  }
   // eslint-disable-next-line security/detect-non-literal-fs-filename
   fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body);
 
@@ -3592,12 +3589,8 @@ with search and pagination: page=1, perPage=2 and DESC sorting`,
     }
     t.is(getTableCsvResponse.status, 201);
     const fileName = `${testTableName}.csv`;
-    const downloadedFilePatch = join(__dirname, 'response-files', fileName);
+    const downloadedFilePatch = join(os.tmpdir(), fileName);
 
-    const dir = join(__dirname, 'response-files');
-    if (!fs.existsSync(dir)) {
-      fs.mkdirSync(dir);
-    }
     // eslint-disable-next-line security/detect-non-literal-fs-filename
     fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body);
 
diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-redis-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-redis-e2e.test.ts
index 947405bd5..679b792d0 100644
--- a/backend/test/ava-tests/non-saas-tests/non-saas-table-redis-e2e.test.ts
+++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-redis-e2e.test.ts
@@ -26,6 +26,7 @@ import { getTestData } from '../../utils/get-test-data.js';
 import { registerUserAndReturnUserInfo } from '../../utils/register-user-and-return-user-info.js';
 import { TestUtils } from '../../utils/test.utils.js';
 import { setSaasEnvVariable } from '../../utils/set-saas-env-variable.js';
+import os from 'node:os';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
@@ -3517,12 +3518,8 @@ test.serial(`${currentTest} should return csv file with table data`, async (t) =
   }
   t.is(getTableCsvResponse.status, 201);
   const fileName = `${testTableName}.csv`;
-  const downloadedFilePatch = join(__dirname, 'response-files', fileName);
+  const downloadedFilePatch = join(os.tmpdir(), fileName);
 
-  const dir = join(__dirname, 'response-files');
-  if (!fs.existsSync(dir)) {
-    fs.mkdirSync(dir);
-  }
   // eslint-disable-next-line security/detect-non-literal-fs-filename
   fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body);
 
@@ -3592,12 +3589,8 @@ with search and pagination: page=1, perPage=2 and DESC sorting`,
     }
     t.is(getTableCsvResponse.status, 201);
     const fileName = `${testTableName}.csv`;
-    const downloadedFilePatch = join(__dirname, 'response-files', fileName);
+    const downloadedFilePatch = join(os.tmpdir(), fileName);
 
-    const dir = join(__dirname, 'response-files');
-    if (!fs.existsSync(dir)) {
-      fs.mkdirSync(dir);
-    }
     // eslint-disable-next-line security/detect-non-literal-fs-filename
     fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body);
 
@@ -3662,12 +3655,8 @@ test.serial(`${currentTest} should import csv file with table data`, async (t) =
   }
   t.is(getTableCsvResponse.status, 201);
   const fileName = `${testTableName}.csv`;
-  const downloadedFilePatch = join(__dirname, 'response-files', fileName);
+  const downloadedFilePatch = join(os.tmpdir(), fileName);
 
-  const dir = join(__dirname, 'response-files');
-  if (!fs.existsSync(dir)) {
-    fs.mkdirSync(dir);
-  }
   // eslint-disable-next-line security/detect-non-literal-fs-filename
   fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body);
   // eslint-disable-next-line security/detect-non-literal-fs-filename

From eb311da413e9a69e6ee22ebca691d9e601342a70 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Sat, 29 Nov 2025 21:16:28 +0000
Subject: [PATCH 12/23] fix flaky test

---
 ...n-saas-user-with-table-only-permissions-e2e.test.ts |  2 ++
 docker-compose.tst.yml                                 | 10 +++-------
 justfile                                               |  2 +-
 3 files changed, 6 insertions(+), 8 deletions(-)

diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-user-with-table-only-permissions-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-user-with-table-only-permissions-e2e.test.ts
index 5d13dc824..91b79cb3a 100644
--- a/backend/test/ava-tests/non-saas-tests/non-saas-user-with-table-only-permissions-e2e.test.ts
+++ b/backend/test/ava-tests/non-saas-tests/non-saas-user-with-table-only-permissions-e2e.test.ts
@@ -2727,6 +2727,8 @@ test.serial(`${currentTest} should return array of table widgets for table`, asy
     const getTableStructureRO = JSON.parse(getTableStructureResponse.text);
     t.is(getTableStructureRO.hasOwnProperty('table_widgets'), true);
     t.is(getTableStructureRO.table_widgets.length, 2);
+    getTableStructureRO.table_widgets.sort((a, b) => a.field_name.localeCompare(b.field_name))
+    newTableWidgets.sort((a, b) => a.field_name.localeCompare(b.field_name))
     t.is(getTableStructureRO.table_widgets[0].field_name, newTableWidgets[0].field_name);
     t.is(getTableStructureRO.table_widgets[1].widget_type, newTableWidgets[1].widget_type);
     t.is(compareTableWidgetsArrays(getTableStructureRO.table_widgets, newTableWidgets), true);
diff --git a/docker-compose.tst.yml b/docker-compose.tst.yml
index ffd0268f2..c2caf07e9 100644
--- a/docker-compose.tst.yml
+++ b/docker-compose.tst.yml
@@ -35,8 +35,8 @@ services:
       context: .
     env_file: ./backend/.development.env
     environment:
-      DATABASE_URL: "postgres://postgres:abc123@postgres:5432/postgres"
-      EXTRA_ARGS: "${EXTRA_ARGS:-}"
+      - "DATABASE_URL=postgres://postgres:abc123@postgres:5432/postgres"
+      - "EXTRA_ARGS=$EXTRA_ARGS"
     volumes:
       - ./backend/src/migrations:/app/src/migrations
     depends_on:
@@ -60,7 +60,7 @@ services:
         condition: service_healthy
       test-cassandra-e2e-testing:
         condition: service_healthy
-    command: ["/bin/sh", "-c", "yarn test $EXTRA_ARGS"]
+    command: ["/bin/sh", "-c", "yarn test-all $EXTRA_ARGS"]
     develop:
       watch:
         - action: rebuild
@@ -169,8 +169,6 @@ services:
       - backend
   test-cassandra-e2e-testing:
     image: cassandra:5.0.4
-    ports:
-      - 9042:9042
     environment:
       - CASSANDRA_CLUSTER_NAME=TestCluster
       - CASSANDRA_DC=TestDC
@@ -327,8 +325,6 @@ services:
     environment:
       - AWS_ACCESS_KEY_ID=SuperSecretAwsAccessKey
       - AWS_SECRET=SuperSecretAwsSecret
-    ports:
-      - 8000:8000
     healthcheck:
       test: ["CMD-SHELL", "curl -s http://localhost:8000 || exit 1"]
       interval: 10s
diff --git a/justfile b/justfile
index 507382c7e..c46044740 100644
--- a/justfile
+++ b/justfile
@@ -1,2 +1,2 @@
-test args='':
+test args='test/ava-tests/non-saas-tests/*':
   EXTRA_ARGS="{{args}}" docker compose  -f docker-compose.tst.yml up --abort-on-container-exit --force-recreate --build --attach=backend_test --no-log-prefix

From 9e1ff271b3ee17711689f4bf353ac6127bf68b61 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Sat, 29 Nov 2025 21:48:17 +0000
Subject: [PATCH 13/23] fix flaky tests

---
 .../non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts  | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts
index 098515fa1..0c751dd18 100644
--- a/backend/test/ava-tests/non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts
+++ b/backend/test/ava-tests/non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts
@@ -3597,6 +3597,8 @@ test.serial(`${currentTest} should return array of table widgets for table`, asy
     const getTableWidgetsRO = JSON.parse(getTableWidgets.text);
     t.is(typeof getTableWidgetsRO, 'object');
     t.is(getTableWidgetsRO.length, 2);
+    getTableWidgetsRO.sort((a, b) => a.field_name.localeCompare(b.field_name))
+    newTableWidgets.sort((a, b) => a.field_name.localeCompare(b.field_name))
 
     t.is(getTableWidgetsRO[0].field_name, newTableWidgets[0].field_name);
     t.is(getTableWidgetsRO[0].widget_type, newTableWidgets[0].widget_type);

From c9f918512fd973b9b5e647c91e3be0bc4f7fd667 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Sat, 29 Nov 2025 22:13:35 +0000
Subject: [PATCH 14/23] fix flaky tests

---
 .../non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts  | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts
index 0c751dd18..2556d873b 100644
--- a/backend/test/ava-tests/non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts
+++ b/backend/test/ava-tests/non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts
@@ -3613,6 +3613,8 @@ test.serial(`${currentTest} should return array of table widgets for table`, asy
     t.is(getTableStructureResponse.status, 200);
     const getTableStructureRO = JSON.parse(getTableStructureResponse.text);
     t.is(getTableStructureRO.hasOwnProperty('table_widgets'), true);
+    getTableStructureRO.table_widgets.sort((a, b) => a.field_name.localeCompare(b.field_name))
+
     t.is(getTableStructureRO.table_widgets.length, 2);
     t.is(getTableStructureRO.table_widgets[0].field_name, newTableWidgets[0].field_name);
     t.is(getTableStructureRO.table_widgets[1].widget_type, newTableWidgets[1].widget_type);

From ab4a5b39dda066a340b92e5a2b7fb2103c2ba21a Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Thu, 4 Dec 2025 13:06:52 +0000
Subject: [PATCH 15/23] swagger, correct response type

---
 .../application/dto/audit-log.dto.ts          | 119 +++++++++++++++---
 .../application/dto/create-secret.dto.ts      |  42 ++++++-
 .../application/dto/found-secret.dto.ts       |  57 +++++++--
 .../application/dto/secret-list.dto.ts        |  97 +++++++++++---
 .../application/dto/update-secret.dto.ts      |  16 ++-
 .../use-cases/create-secret.use.case.ts       |   4 +-
 .../use-cases/delete-secret.use.case.ts       |   4 +-
 .../get-secret-audit-log.use.case.ts          |   4 +-
 .../use-cases/get-secret-by-slug.use.case.ts  |   2 +-
 .../use-cases/get-secrets.use.case.ts         |   4 +-
 .../use-cases/update-secret.use.case.ts       |   2 +-
 .../user-secret/user-secret.controller.ts     |  58 ++++++---
 12 files changed, 327 insertions(+), 82 deletions(-)

diff --git a/backend/src/entities/user-secret/application/dto/audit-log.dto.ts b/backend/src/entities/user-secret/application/dto/audit-log.dto.ts
index a22f2c215..609972ea9 100644
--- a/backend/src/entities/user-secret/application/dto/audit-log.dto.ts
+++ b/backend/src/entities/user-secret/application/dto/audit-log.dto.ts
@@ -1,44 +1,123 @@
 import { ApiProperty } from '@nestjs/swagger';
 import { SecretActionEnum } from '../../../secret-access-log/secret-access-log.entity.js';
 
+export class AuditLogUserDto {
+  @ApiProperty({
+    type: String,
+    description: 'User ID who performed the action',
+    example: '550e8400-e29b-41d4-a716-446655440002',
+  })
+  id: string;
+
+  @ApiProperty({
+    type: String,
+    description: 'Email of the user who performed the action',
+    example: 'user@example.com',
+  })
+  email: string;
+}
+
 export class AuditLogEntryDto {
-  @ApiProperty()
+  @ApiProperty({
+    type: String,
+    description: 'Unique identifier of the audit log entry',
+    example: '550e8400-e29b-41d4-a716-446655440003',
+  })
   id: string;
 
-  @ApiProperty({ enum: SecretActionEnum })
+  @ApiProperty({
+    enum: SecretActionEnum,
+    enumName: 'SecretActionEnum',
+    description: 'Type of action performed on the secret',
+    example: SecretActionEnum.VIEW,
+  })
   action: SecretActionEnum;
 
-  @ApiProperty()
-  user: {
-    id: string;
-    email: string;
-  };
+  @ApiProperty({
+    type: AuditLogUserDto,
+    description: 'User who performed the action',
+  })
+  user: AuditLogUserDto;
 
-  @ApiProperty()
+  @ApiProperty({
+    type: Date,
+    description: 'Date and time when the action was performed',
+    example: '2025-01-25T09:15:00.000Z',
+  })
   accessedAt: Date;
 
-  @ApiProperty({ required: false })
+  @ApiProperty({
+    type: String,
+    required: false,
+    description: 'IP address from which the action was performed',
+    example: '192.168.1.100',
+  })
   ipAddress?: string;
 
-  @ApiProperty({ required: false })
+  @ApiProperty({
+    type: String,
+    required: false,
+    description: 'User agent string of the client',
+    example: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0',
+  })
   userAgent?: string;
 
-  @ApiProperty()
+  @ApiProperty({
+    type: Boolean,
+    description: 'Whether the action was successful',
+    example: true,
+  })
   success: boolean;
 
-  @ApiProperty({ required: false })
+  @ApiProperty({
+    type: String,
+    required: false,
+    description: 'Error message if the action failed',
+    example: 'Invalid master password',
+  })
   errorMessage?: string;
 }
 
+export class AuditLogPaginationDto {
+  @ApiProperty({
+    type: Number,
+    description: 'Total number of audit log entries',
+    example: 150,
+  })
+  total: number;
+
+  @ApiProperty({
+    type: Number,
+    description: 'Current page number',
+    example: 1,
+  })
+  page: number;
+
+  @ApiProperty({
+    type: Number,
+    description: 'Number of entries per page',
+    example: 50,
+  })
+  limit: number;
+
+  @ApiProperty({
+    type: Number,
+    description: 'Total number of pages',
+    example: 3,
+  })
+  totalPages: number;
+}
+
 export class AuditLogResponseDto {
-  @ApiProperty({ type: [AuditLogEntryDto] })
+  @ApiProperty({
+    type: [AuditLogEntryDto],
+    description: 'List of audit log entries',
+  })
   data: AuditLogEntryDto[];
 
-  @ApiProperty()
-  pagination: {
-    total: number;
-    page: number;
-    limit: number;
-    totalPages: number;
-  };
+  @ApiProperty({
+    type: AuditLogPaginationDto,
+    description: 'Pagination information',
+  })
+  pagination: AuditLogPaginationDto;
 }
diff --git a/backend/src/entities/user-secret/application/dto/create-secret.dto.ts b/backend/src/entities/user-secret/application/dto/create-secret.dto.ts
index 93f8b994a..d97c383f2 100644
--- a/backend/src/entities/user-secret/application/dto/create-secret.dto.ts
+++ b/backend/src/entities/user-secret/application/dto/create-secret.dto.ts
@@ -3,7 +3,15 @@ import { Transform } from 'class-transformer';
 import { IsBoolean, IsISO8601, IsNotEmpty, IsOptional, IsString, Matches, MaxLength, MinLength, ValidateIf } from 'class-validator';
 
 export class CreateSecretDto {
-  @ApiProperty({ required: true, type: 'string' })
+  @ApiProperty({
+    type: String,
+    required: true,
+    description: 'Unique identifier for the secret within the company',
+    example: 'database-password',
+    minLength: 1,
+    maxLength: 255,
+    pattern: '^[a-zA-Z0-9_-]+$',
+  })
   @IsString()
   @IsNotEmpty()
   @MinLength(1)
@@ -14,24 +22,48 @@ export class CreateSecretDto {
   @Transform(({ value }) => value.trim())
   slug: string;
 
-  @ApiProperty({ required: true, type: 'string' })
+  @ApiProperty({
+    type: String,
+    required: true,
+    description: 'The secret value to be stored (will be encrypted)',
+    example: 'my-secret-value-123',
+    minLength: 1,
+    maxLength: 10000,
+  })
   @IsString()
   @IsNotEmpty()
   @MinLength(1)
   @MaxLength(10000)
   value: string;
 
-  @ApiProperty({ required: false, type: 'string' })
+  @ApiProperty({
+    type: String,
+    required: false,
+    description: 'ISO 8601 date when the secret should expire',
+    example: '2025-12-31T23:59:59.000Z',
+  })
   @IsOptional()
   @IsISO8601()
   expiresAt?: string;
 
-  @ApiProperty({ required: false, type: 'boolean' })
+  @ApiProperty({
+    type: Boolean,
+    required: false,
+    default: false,
+    description: 'Whether to encrypt the secret with a master password',
+    example: false,
+  })
   @IsBoolean()
   @IsOptional()
   masterEncryption?: boolean;
 
-  @ApiProperty({ required: false, type: 'string' })
+  @ApiProperty({
+    type: String,
+    required: false,
+    description: 'Master password for additional encryption (required if masterEncryption is true)',
+    example: 'master-password-123',
+    minLength: 8,
+  })
   @IsString()
   @MinLength(8)
   @ValidateIf((o) => o.masterEncryption === true)
diff --git a/backend/src/entities/user-secret/application/dto/found-secret.dto.ts b/backend/src/entities/user-secret/application/dto/found-secret.dto.ts
index 9b926610b..65d32668c 100644
--- a/backend/src/entities/user-secret/application/dto/found-secret.dto.ts
+++ b/backend/src/entities/user-secret/application/dto/found-secret.dto.ts
@@ -1,30 +1,69 @@
 import { ApiProperty } from '@nestjs/swagger';
 
 export class FoundSecretDto {
-  @ApiProperty()
+  @ApiProperty({
+    type: String,
+    description: 'Unique identifier of the secret',
+    example: '550e8400-e29b-41d4-a716-446655440000',
+  })
   id: string;
 
-  @ApiProperty()
+  @ApiProperty({
+    type: String,
+    description: 'Unique slug identifier for the secret',
+    example: 'database-password',
+  })
   slug: string;
 
-  @ApiProperty({ required: false })
+  @ApiProperty({
+    type: String,
+    required: false,
+    description: 'Decrypted secret value (only included when retrieving a specific secret)',
+    example: 'my-secret-value-123',
+  })
   value?: string;
 
-  @ApiProperty()
+  @ApiProperty({
+    type: String,
+    description: 'Company ID that owns this secret',
+    example: '550e8400-e29b-41d4-a716-446655440001',
+  })
   companyId: string;
 
-  @ApiProperty()
+  @ApiProperty({
+    type: Date,
+    description: 'Date when the secret was created',
+    example: '2025-01-15T10:30:00.000Z',
+  })
   createdAt: Date;
 
-  @ApiProperty()
+  @ApiProperty({
+    type: Date,
+    description: 'Date when the secret was last updated',
+    example: '2025-01-20T14:45:00.000Z',
+  })
   updatedAt: Date;
 
-  @ApiProperty({ required: false })
+  @ApiProperty({
+    type: Date,
+    required: false,
+    description: 'Date when the secret was last accessed',
+    example: '2025-01-25T09:15:00.000Z',
+  })
   lastAccessedAt?: Date;
 
-  @ApiProperty({ required: false })
+  @ApiProperty({
+    type: Date,
+    required: false,
+    description: 'Date when the secret expires (null if no expiration)',
+    example: '2025-12-31T23:59:59.000Z',
+  })
   expiresAt?: Date;
 
-  @ApiProperty()
+  @ApiProperty({
+    type: Boolean,
+    description: 'Whether the secret requires a master password for decryption',
+    example: false,
+  })
   masterEncryption: boolean;
 }
diff --git a/backend/src/entities/user-secret/application/dto/secret-list.dto.ts b/backend/src/entities/user-secret/application/dto/secret-list.dto.ts
index 4e1568012..b1c817cc5 100644
--- a/backend/src/entities/user-secret/application/dto/secret-list.dto.ts
+++ b/backend/src/entities/user-secret/application/dto/secret-list.dto.ts
@@ -1,40 +1,105 @@
 import { ApiProperty } from '@nestjs/swagger';
 
 export class SecretListItemDto {
-  @ApiProperty()
+  @ApiProperty({
+    type: String,
+    description: 'Unique identifier of the secret',
+    example: '550e8400-e29b-41d4-a716-446655440000',
+  })
   id: string;
 
-  @ApiProperty()
+  @ApiProperty({
+    type: String,
+    description: 'Unique slug identifier for the secret',
+    example: 'database-password',
+  })
   slug: string;
 
-  @ApiProperty()
+  @ApiProperty({
+    type: String,
+    description: 'Company ID that owns this secret',
+    example: '550e8400-e29b-41d4-a716-446655440001',
+  })
   companyId: string;
 
-  @ApiProperty()
+  @ApiProperty({
+    type: Date,
+    description: 'Date when the secret was created',
+    example: '2025-01-15T10:30:00.000Z',
+  })
   createdAt: Date;
 
-  @ApiProperty()
+  @ApiProperty({
+    type: Date,
+    description: 'Date when the secret was last updated',
+    example: '2025-01-20T14:45:00.000Z',
+  })
   updatedAt: Date;
 
-  @ApiProperty({ required: false })
+  @ApiProperty({
+    type: Date,
+    required: false,
+    description: 'Date when the secret was last accessed',
+    example: '2025-01-25T09:15:00.000Z',
+  })
   lastAccessedAt?: Date;
 
-  @ApiProperty({ required: false })
+  @ApiProperty({
+    type: Date,
+    required: false,
+    description: 'Date when the secret expires (null if no expiration)',
+    example: '2025-12-31T23:59:59.000Z',
+  })
   expiresAt?: Date;
 
-  @ApiProperty()
+  @ApiProperty({
+    type: Boolean,
+    description: 'Whether the secret requires a master password for decryption',
+    example: false,
+  })
   masterEncryption: boolean;
 }
 
+export class PaginationDto {
+  @ApiProperty({
+    type: Number,
+    description: 'Total number of items',
+    example: 42,
+  })
+  total: number;
+
+  @ApiProperty({
+    type: Number,
+    description: 'Current page number',
+    example: 1,
+  })
+  page: number;
+
+  @ApiProperty({
+    type: Number,
+    description: 'Number of items per page',
+    example: 20,
+  })
+  limit: number;
+
+  @ApiProperty({
+    type: Number,
+    description: 'Total number of pages',
+    example: 3,
+  })
+  totalPages: number;
+}
+
 export class SecretListResponseDto {
-  @ApiProperty({ type: [SecretListItemDto] })
+  @ApiProperty({
+    type: [SecretListItemDto],
+    description: 'List of secrets',
+  })
   data: SecretListItemDto[];
 
-  @ApiProperty()
-  pagination: {
-    total: number;
-    page: number;
-    limit: number;
-    totalPages: number;
-  };
+  @ApiProperty({
+    type: PaginationDto,
+    description: 'Pagination information',
+  })
+  pagination: PaginationDto;
 }
diff --git a/backend/src/entities/user-secret/application/dto/update-secret.dto.ts b/backend/src/entities/user-secret/application/dto/update-secret.dto.ts
index 91dc16596..4037bc013 100644
--- a/backend/src/entities/user-secret/application/dto/update-secret.dto.ts
+++ b/backend/src/entities/user-secret/application/dto/update-secret.dto.ts
@@ -2,14 +2,26 @@ import { ApiProperty } from '@nestjs/swagger';
 import { IsISO8601, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
 
 export class UpdateSecretDto {
-  @ApiProperty({ required: true, type: 'string' })
+  @ApiProperty({
+    type: String,
+    required: true,
+    description: 'The new secret value (will be encrypted)',
+    example: 'my-updated-secret-value-456',
+    minLength: 1,
+    maxLength: 10000,
+  })
   @IsString()
   @IsNotEmpty()
   @MinLength(1)
   @MaxLength(10000)
   value: string;
 
-  @ApiProperty({ required: false, type: 'string' })
+  @ApiProperty({
+    type: String,
+    required: false,
+    description: 'ISO 8601 date when the secret should expire (null to remove expiration)',
+    example: '2025-12-31T23:59:59.000Z',
+  })
   @IsOptional()
   @IsISO8601()
   expiresAt?: string;
diff --git a/backend/src/entities/user-secret/use-cases/create-secret.use.case.ts b/backend/src/entities/user-secret/use-cases/create-secret.use.case.ts
index 94b63a7ea..793ea0513 100644
--- a/backend/src/entities/user-secret/use-cases/create-secret.use.case.ts
+++ b/backend/src/entities/user-secret/use-cases/create-secret.use.case.ts
@@ -1,4 +1,4 @@
-import { ConflictException, ForbiddenException, Inject, Injectable, Scope } from '@nestjs/common';
+import { ConflictException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
 import AbstractUseCase from '../../../common/abstract-use.case.js';
 import { BaseType } from '../../../common/data-injection.tokens.js';
 import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
@@ -28,7 +28,7 @@ export class CreateSecretUseCase extends AbstractUseCase {
@@ -92,9 +100,14 @@ export class UserSecretController {
     description: 'Returns list of company secrets.',
     type: SecretListResponseDto,
   })
-  @ApiQuery({ name: 'page', required: false, type: Number })
-  @ApiQuery({ name: 'limit', required: false, type: Number })
-  @ApiQuery({ name: 'search', required: false, type: String })
+  @ApiResponse({
+    status: 404,
+    description: 'User not found or not in a company.',
+  })
+  @ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default: 1)' })
+  @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page (default: 20)' })
+  @ApiQuery({ name: 'search', required: false, type: String, description: 'Search term to filter secrets by slug' })
+  @UseGuards(CompanyUserGuard)
   @Get('/secrets')
   async getSecrets(
     @UserId() userId: string,
@@ -123,12 +136,14 @@ export class UserSecretController {
   })
   @ApiResponse({
     status: 404,
-    description: 'Secret not found.',
+    description: 'Secret or user not found.',
   })
   @ApiResponse({
     status: 410,
     description: 'Secret has expired.',
   })
+  @ApiParam({ name: 'slug', type: String, description: 'Unique secret identifier', example: 'database-password' })
+  @UseGuards(CompanyUserGuard)
   @Get('/secrets/:slug')
   async getSecretBySlug(
     @UserId() userId: string,
@@ -151,12 +166,18 @@ export class UserSecretController {
   })
   @ApiResponse({
     status: 403,
-    description: "You don't have permission to modify this secret.",
+    description: 'Master password required or incorrect.',
   })
   @ApiResponse({
     status: 404,
-    description: 'Secret not found.',
+    description: 'Secret or user not found.',
   })
+  @ApiResponse({
+    status: 410,
+    description: 'Secret has expired.',
+  })
+  @ApiParam({ name: 'slug', type: String, description: 'Unique secret identifier', example: 'database-password' })
+  @UseGuards(CompanyUserGuard)
   @Put('/secrets/:slug')
   async updateSecret(
     @UserId() userId: string,
@@ -181,17 +202,16 @@ export class UserSecretController {
   @ApiResponse({
     status: 200,
     description: 'Secret deleted successfully.',
-  })
-  @ApiResponse({
-    status: 403,
-    description: "You don't have permission to delete this secret.",
+    type: DeleteSecretResponseDto,
   })
   @ApiResponse({
     status: 404,
-    description: 'Secret not found.',
+    description: 'Secret or user not found.',
   })
+  @ApiParam({ name: 'slug', type: String, description: 'Unique secret identifier', example: 'database-password' })
+  @UseGuards(CompanyUserGuard)
   @Delete('/secrets/:slug')
-  async deleteSecret(@UserId() userId: string, @Param('slug') slug: string): Promise<{ message: string; deletedAt: Date }> {
+  async deleteSecret(@UserId() userId: string, @Param('slug') slug: string): Promise {
     return await this.deleteSecretUseCase.execute({ userId, slug }, InTransactionEnum.ON);
   }
 
@@ -201,16 +221,14 @@ export class UserSecretController {
     description: 'Returns audit log for the secret.',
     type: AuditLogResponseDto,
   })
-  @ApiResponse({
-    status: 403,
-    description: "You don't have permission to view this audit log.",
-  })
   @ApiResponse({
     status: 404,
-    description: 'Secret not found.',
+    description: 'Secret or user not found.',
   })
-  @ApiQuery({ name: 'page', required: false, type: Number })
-  @ApiQuery({ name: 'limit', required: false, type: Number })
+  @ApiParam({ name: 'slug', type: String, description: 'Unique secret identifier', example: 'database-password' })
+  @ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default: 1)' })
+  @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page (default: 50)' })
+  @UseGuards(CompanyUserGuard)
   @Get('/secrets/:slug/audit-log')
   async getAuditLog(
     @UserId() userId: string,

From e207ba56b4a5052d9ecc871cf7df15edea749fd9 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Thu, 4 Dec 2025 13:15:22 +0000
Subject: [PATCH 16/23] reverse relations

---
 backend/src/entities/company-info/company-info.entity.ts    | 6 ++++++
 .../entities/secret-access-log/secret-access-log.entity.ts  | 2 +-
 backend/src/entities/user-secret/user-secret.entity.ts      | 2 +-
 backend/src/entities/user/user.entity.ts                    | 4 ++++
 4 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/backend/src/entities/company-info/company-info.entity.ts b/backend/src/entities/company-info/company-info.entity.ts
index 945cde5d5..44516b69c 100644
--- a/backend/src/entities/company-info/company-info.entity.ts
+++ b/backend/src/entities/company-info/company-info.entity.ts
@@ -6,6 +6,7 @@ import { generateCompanyName } from './utils/get-company-name.js';
 import { CompanyLogoEntity } from '../company-logo/company-logo.entity.js';
 import { CompanyFaviconEntity } from '../company-favicon/company-favicon.entity.js';
 import { CompanyTabTitleEntity } from '../company-tab-title/company-tab-title.entity.js';
+import { UserSecretEntity } from '../user-secret/user-secret.entity.js';
 
 @Entity('company_info')
 export class CompanyInfoEntity {
@@ -60,4 +61,9 @@ export class CompanyInfoEntity {
     onDelete: 'NO ACTION',
   })
   tab_title: Relation;
+
+  @OneToMany((_) => UserSecretEntity, (secret) => secret.company, {
+    onDelete: 'NO ACTION',
+  })
+  secrets: Relation[];
 }
diff --git a/backend/src/entities/secret-access-log/secret-access-log.entity.ts b/backend/src/entities/secret-access-log/secret-access-log.entity.ts
index 35cc02744..9e1cbd40e 100644
--- a/backend/src/entities/secret-access-log/secret-access-log.entity.ts
+++ b/backend/src/entities/secret-access-log/secret-access-log.entity.ts
@@ -23,7 +23,7 @@ export class SecretAccessLogEntity {
   @Index()
   secretId: string;
 
-  @ManyToOne(() => UserEntity, { onDelete: 'CASCADE' })
+  @ManyToOne(() => UserEntity, (user) => user.secretAccessLogs, { onDelete: 'CASCADE' })
   @JoinColumn({ name: 'userId' })
   user: Relation;
 
diff --git a/backend/src/entities/user-secret/user-secret.entity.ts b/backend/src/entities/user-secret/user-secret.entity.ts
index 89ea242a5..1a9faaddf 100644
--- a/backend/src/entities/user-secret/user-secret.entity.ts
+++ b/backend/src/entities/user-secret/user-secret.entity.ts
@@ -19,7 +19,7 @@ export class UserSecretEntity {
   @PrimaryGeneratedColumn('uuid')
   id: string;
 
-  @ManyToOne(() => CompanyInfoEntity, { onDelete: 'CASCADE' })
+  @ManyToOne(() => CompanyInfoEntity, (company) => company.secrets, { onDelete: 'CASCADE' })
   @JoinColumn({ name: 'companyId' })
   company: Relation;
 
diff --git a/backend/src/entities/user/user.entity.ts b/backend/src/entities/user/user.entity.ts
index 91e5d2906..f73a96ed9 100644
--- a/backend/src/entities/user/user.entity.ts
+++ b/backend/src/entities/user/user.entity.ts
@@ -27,6 +27,7 @@ import { ExternalRegistrationProviderEnum } from './enums/external-registration-
 import { UserApiKeyEntity } from '../api-key/api-key.entity.js';
 import { AiResponsesToUserEntity } from '../ai/ai-data-entities/ai-reponses-to-user/ai-responses-to-user.entity.js';
 import { SignInAuditEntity } from '../user-sign-in-audit/sign-in-audit.entity.js';
+import { SecretAccessLogEntity } from '../secret-access-log/secret-access-log.entity.js';
 
 @Entity('user')
 export class UserEntity {
@@ -123,6 +124,9 @@ export class UserEntity {
   @OneToMany((_) => SignInAuditEntity, (signInAudit) => signInAudit.user)
   signInAudits: Relation[];
 
+  @OneToMany((_) => SecretAccessLogEntity, (secretAccessLog) => secretAccessLog.user)
+  secretAccessLogs: Relation[];
+
   @Column({ default: false, type: 'boolean' })
   isActive: boolean;
 

From 42fb55e5c1e43da18cdf56da6728e4d9596f15f7 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Thu, 4 Dec 2025 13:27:48 +0000
Subject: [PATCH 17/23] missing file

---
 .../application/dto/delete-secret.dto.ts        | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)
 create mode 100644 backend/src/entities/user-secret/application/dto/delete-secret.dto.ts

diff --git a/backend/src/entities/user-secret/application/dto/delete-secret.dto.ts b/backend/src/entities/user-secret/application/dto/delete-secret.dto.ts
new file mode 100644
index 000000000..de98cd93d
--- /dev/null
+++ b/backend/src/entities/user-secret/application/dto/delete-secret.dto.ts
@@ -0,0 +1,17 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class DeleteSecretResponseDto {
+  @ApiProperty({
+    type: String,
+    description: 'Confirmation message',
+    example: 'Secret deleted successfully',
+  })
+  message: string;
+
+  @ApiProperty({
+    type: Date,
+    description: 'Date and time when the secret was deleted',
+    example: '2025-01-25T10:30:00.000Z',
+  })
+  deletedAt: Date;
+}

From afad5331d47f321929ea33f5f192e42c2bec3caa Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Thu, 4 Dec 2025 13:48:31 +0000
Subject: [PATCH 18/23] proper types for this, coding conventions

---
 CLAUDE.md                                                 | 6 ++++++
 .../repository/secret-access-log-repository.extension.ts  | 8 +++-----
 .../repository/secret-access-log-repository.interface.ts  | 3 +++
 3 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 27e034ffb..2e7570e3f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -124,3 +124,9 @@ Test databases are defined in `docker-compose.tst.yml`:
 - IBM DB2: `test-ibm-db2-e2e-testing:50000`
 - MongoDB: `test-mongo-e2e-testing:27017`
 - DynamoDB: `test-dynamodb-e2e-testing:8000`
+
+## Coding Conventions
+
+### Class Member Ordering
+
+- Private methods must be placed at the end of the class, after all public methods
diff --git a/backend/src/entities/secret-access-log/repository/secret-access-log-repository.extension.ts b/backend/src/entities/secret-access-log/repository/secret-access-log-repository.extension.ts
index 5ed5f6cb7..2e6d84c00 100644
--- a/backend/src/entities/secret-access-log/repository/secret-access-log-repository.extension.ts
+++ b/backend/src/entities/secret-access-log/repository/secret-access-log-repository.extension.ts
@@ -9,8 +9,7 @@ export const secretAccessLogRepositoryExtension: ISecretAccessLogRepository = {
     success: boolean = true,
     errorMessage?: string,
   ): Promise {
-    const self = this as any;
-    const log = self.create({
+    const log = this.create({
       secretId,
       userId,
       action,
@@ -18,15 +17,14 @@ export const secretAccessLogRepositoryExtension: ISecretAccessLogRepository = {
       errorMessage,
       accessedAt: new Date(),
     });
-    return self.save(log);
+    return this.save(log);
   },
 
   async findLogsForSecret(
     secretId: string,
     options: { page: number; limit: number },
   ): Promise<[SecretAccessLogEntity[], number]> {
-    const self = this as any;
-    return self.findAndCount({
+    return this.findAndCount({
       where: {
         secretId,
       },
diff --git a/backend/src/entities/secret-access-log/repository/secret-access-log-repository.interface.ts b/backend/src/entities/secret-access-log/repository/secret-access-log-repository.interface.ts
index e3228f743..374bab0d4 100644
--- a/backend/src/entities/secret-access-log/repository/secret-access-log-repository.interface.ts
+++ b/backend/src/entities/secret-access-log/repository/secret-access-log-repository.interface.ts
@@ -1,7 +1,9 @@
+import { Repository } from 'typeorm';
 import { SecretAccessLogEntity, SecretActionEnum } from '../secret-access-log.entity.js';
 
 export interface ISecretAccessLogRepository {
   createAccessLog(
+    this: Repository,
     secretId: string,
     userId: string,
     action: SecretActionEnum,
@@ -9,6 +11,7 @@ export interface ISecretAccessLogRepository {
     errorMessage?: string,
   ): Promise;
   findLogsForSecret(
+    this: Repository,
     secretId: string,
     options: { page: number; limit: number },
   ): Promise<[SecretAccessLogEntity[], number]>;

From 21dca8bef35b9ccd9a4a8636c731603d5b1bde6b Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Thu, 4 Dec 2025 13:52:35 +0000
Subject: [PATCH 19/23] audit dto fix

---
 .../src/entities/user-secret/application/dto/audit-log.dto.ts  | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/backend/src/entities/user-secret/application/dto/audit-log.dto.ts b/backend/src/entities/user-secret/application/dto/audit-log.dto.ts
index 609972ea9..ecc4b4578 100644
--- a/backend/src/entities/user-secret/application/dto/audit-log.dto.ts
+++ b/backend/src/entities/user-secret/application/dto/audit-log.dto.ts
@@ -110,7 +110,8 @@ export class AuditLogPaginationDto {
 
 export class AuditLogResponseDto {
   @ApiProperty({
-    type: [AuditLogEntryDto],
+    type: AuditLogEntryDto,
+    isArray: true,
     description: 'List of audit log entries',
   })
   data: AuditLogEntryDto[];

From 84cb484bc765b1c9937bdd0ce4f10eb1915c12ab Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Thu, 4 Dec 2025 14:49:01 +0000
Subject: [PATCH 20/23] apibody guards, pagination dto

---
 .../data-structures/get-secrets.ds.ts         |  9 ++---
 .../application/dto/secret-list.dto.ts        | 35 ++-----------------
 .../use-cases/get-secrets.use.case.ts         |  6 ++--
 .../user-secret/user-secret.controller.ts     |  4 ++-
 .../non-saas-secrets-e2e.test.ts              |  4 +--
 5 files changed, 14 insertions(+), 44 deletions(-)

diff --git a/backend/src/entities/user-secret/application/data-structures/get-secrets.ds.ts b/backend/src/entities/user-secret/application/data-structures/get-secrets.ds.ts
index 1cfa962a5..8b68bf4a4 100644
--- a/backend/src/entities/user-secret/application/data-structures/get-secrets.ds.ts
+++ b/backend/src/entities/user-secret/application/data-structures/get-secrets.ds.ts
@@ -1,3 +1,5 @@
+import { PaginationDs } from '../../../table/application/data-structures/pagination.ds.js';
+
 export class GetSecretsDS {
   userId: string;
   page: number;
@@ -18,10 +20,5 @@ export class SecretListItemDS {
 
 export class SecretsListDS {
   data: SecretListItemDS[];
-  pagination: {
-    total: number;
-    page: number;
-    limit: number;
-    totalPages: number;
-  };
+  pagination: PaginationDs;
 }
diff --git a/backend/src/entities/user-secret/application/dto/secret-list.dto.ts b/backend/src/entities/user-secret/application/dto/secret-list.dto.ts
index b1c817cc5..eaf235cfc 100644
--- a/backend/src/entities/user-secret/application/dto/secret-list.dto.ts
+++ b/backend/src/entities/user-secret/application/dto/secret-list.dto.ts
@@ -1,4 +1,5 @@
 import { ApiProperty } from '@nestjs/swagger';
+import { PaginationDs } from '../../../table/application/data-structures/pagination.ds.js';
 
 export class SecretListItemDto {
   @ApiProperty({
@@ -60,36 +61,6 @@ export class SecretListItemDto {
   masterEncryption: boolean;
 }
 
-export class PaginationDto {
-  @ApiProperty({
-    type: Number,
-    description: 'Total number of items',
-    example: 42,
-  })
-  total: number;
-
-  @ApiProperty({
-    type: Number,
-    description: 'Current page number',
-    example: 1,
-  })
-  page: number;
-
-  @ApiProperty({
-    type: Number,
-    description: 'Number of items per page',
-    example: 20,
-  })
-  limit: number;
-
-  @ApiProperty({
-    type: Number,
-    description: 'Total number of pages',
-    example: 3,
-  })
-  totalPages: number;
-}
-
 export class SecretListResponseDto {
   @ApiProperty({
     type: [SecretListItemDto],
@@ -98,8 +69,8 @@ export class SecretListResponseDto {
   data: SecretListItemDto[];
 
   @ApiProperty({
-    type: PaginationDto,
+    type: PaginationDs,
     description: 'Pagination information',
   })
-  pagination: PaginationDto;
+  pagination: PaginationDs;
 }
diff --git a/backend/src/entities/user-secret/use-cases/get-secrets.use.case.ts b/backend/src/entities/user-secret/use-cases/get-secrets.use.case.ts
index 03733bf75..947aa78f9 100644
--- a/backend/src/entities/user-secret/use-cases/get-secrets.use.case.ts
+++ b/backend/src/entities/user-secret/use-cases/get-secrets.use.case.ts
@@ -38,9 +38,9 @@ export class GetSecretsUseCase extends AbstractUseCase buildSecretListItemDS(secret)),
       pagination: {
         total,
-        page,
-        limit,
-        totalPages: Math.ceil(total / limit),
+        currentPage: page,
+        perPage: limit,
+        lastPage: Math.ceil(total / limit),
       },
     };
   }
diff --git a/backend/src/entities/user-secret/user-secret.controller.ts b/backend/src/entities/user-secret/user-secret.controller.ts
index c5d847d59..abdd2ec76 100644
--- a/backend/src/entities/user-secret/user-secret.controller.ts
+++ b/backend/src/entities/user-secret/user-secret.controller.ts
@@ -14,7 +14,7 @@ import {
   UseGuards,
   UseInterceptors,
 } from '@nestjs/common';
-import { ApiBearerAuth, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
+import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
 import { UserId } from '../../decorators/user-id.decorator.js';
 import { MasterPassword } from '../../decorators/master-password.decorator.js';
 import { SentryInterceptor } from '../../interceptors/sentry.interceptor.js';
@@ -63,6 +63,7 @@ export class UserSecretController {
   ) {}
 
   @ApiOperation({ summary: 'Create new secret' })
+  @ApiBody({ type: CreateSecretDto })
   @ApiResponse({
     status: 201,
     description: 'Secret created successfully.',
@@ -159,6 +160,7 @@ export class UserSecretController {
   }
 
   @ApiOperation({ summary: 'Update secret' })
+  @ApiBody({ type: UpdateSecretDto })
   @ApiResponse({
     status: 200,
     description: 'Secret updated successfully.',
diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
index 64e4c9ea9..11a92fefa 100644
--- a/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
+++ b/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
@@ -178,7 +178,7 @@ test.serial(`${currentTest} - should return list of company secrets`, async (t)
   t.truthy(responseBody.data);
   t.true(Array.isArray(responseBody.data));
   t.truthy(responseBody.pagination);
-  t.is(responseBody.pagination.page, 1);
+  t.is(responseBody.pagination.currentPage, 1);
   t.true(responseBody.data.length > 0);
 
   const firstSecret = responseBody.data[0];
@@ -495,7 +495,7 @@ test.serial(`${currentTest}?page=1&limit=2 - should paginate audit log`, async (
 
   t.is(response.status, 200, response.text);
   const responseBody = JSON.parse(response.text);
-  t.is(responseBody.pagination.limit, '2');
+  t.is(responseBody.pagination.perPage, 2);
   t.true(responseBody.data.length <= 2);
 });
 

From 864f47cc0ca47add9e756e75c3e0ed1c14c19802 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Thu, 4 Dec 2025 14:56:39 +0000
Subject: [PATCH 21/23] missing isArray

---
 .../entities/user-secret/application/dto/secret-list.dto.ts    | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/backend/src/entities/user-secret/application/dto/secret-list.dto.ts b/backend/src/entities/user-secret/application/dto/secret-list.dto.ts
index eaf235cfc..a768c69f5 100644
--- a/backend/src/entities/user-secret/application/dto/secret-list.dto.ts
+++ b/backend/src/entities/user-secret/application/dto/secret-list.dto.ts
@@ -63,7 +63,8 @@ export class SecretListItemDto {
 
 export class SecretListResponseDto {
   @ApiProperty({
-    type: [SecretListItemDto],
+    type: SecretListItemDto,
+    isArray: true,
     description: 'List of secrets',
   })
   data: SecretListItemDto[];

From 21289082ba4469ecc9690f97698707c556e8cb62 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Thu, 4 Dec 2025 15:24:48 +0000
Subject: [PATCH 22/23] pagination fix

---
 .../data-structures/get-audit-log.ds.ts       |  8 ++---
 .../application/dto/audit-log.dto.ts          | 35 ++-----------------
 .../get-secret-audit-log.use.case.ts          |  6 ++--
 .../non-saas-secrets-e2e.test.ts              |  2 --
 4 files changed, 8 insertions(+), 43 deletions(-)

diff --git a/backend/src/entities/user-secret/application/data-structures/get-audit-log.ds.ts b/backend/src/entities/user-secret/application/data-structures/get-audit-log.ds.ts
index 64a9d7b35..e329a5c44 100644
--- a/backend/src/entities/user-secret/application/data-structures/get-audit-log.ds.ts
+++ b/backend/src/entities/user-secret/application/data-structures/get-audit-log.ds.ts
@@ -1,4 +1,5 @@
 import { SecretActionEnum } from '../../../secret-access-log/secret-access-log.entity.js';
+import { PaginationDs } from '../../../table/application/data-structures/pagination.ds.js';
 
 export class GetAuditLogDS {
   userId: string;
@@ -23,10 +24,5 @@ export class AuditLogEntryDS {
 
 export class AuditLogListDS {
   data: AuditLogEntryDS[];
-  pagination: {
-    total: number;
-    page: number;
-    limit: number;
-    totalPages: number;
-  };
+  pagination: PaginationDs;
 }
diff --git a/backend/src/entities/user-secret/application/dto/audit-log.dto.ts b/backend/src/entities/user-secret/application/dto/audit-log.dto.ts
index ecc4b4578..5585490b7 100644
--- a/backend/src/entities/user-secret/application/dto/audit-log.dto.ts
+++ b/backend/src/entities/user-secret/application/dto/audit-log.dto.ts
@@ -1,5 +1,6 @@
 import { ApiProperty } from '@nestjs/swagger';
 import { SecretActionEnum } from '../../../secret-access-log/secret-access-log.entity.js';
+import { PaginationDs } from '../../../table/application/data-structures/pagination.ds.js';
 
 export class AuditLogUserDto {
   @ApiProperty({
@@ -78,36 +79,6 @@ export class AuditLogEntryDto {
   errorMessage?: string;
 }
 
-export class AuditLogPaginationDto {
-  @ApiProperty({
-    type: Number,
-    description: 'Total number of audit log entries',
-    example: 150,
-  })
-  total: number;
-
-  @ApiProperty({
-    type: Number,
-    description: 'Current page number',
-    example: 1,
-  })
-  page: number;
-
-  @ApiProperty({
-    type: Number,
-    description: 'Number of entries per page',
-    example: 50,
-  })
-  limit: number;
-
-  @ApiProperty({
-    type: Number,
-    description: 'Total number of pages',
-    example: 3,
-  })
-  totalPages: number;
-}
-
 export class AuditLogResponseDto {
   @ApiProperty({
     type: AuditLogEntryDto,
@@ -117,8 +88,8 @@ export class AuditLogResponseDto {
   data: AuditLogEntryDto[];
 
   @ApiProperty({
-    type: AuditLogPaginationDto,
+    type: PaginationDs,
     description: 'Pagination information',
   })
-  pagination: AuditLogPaginationDto;
+  pagination: PaginationDs;
 }
diff --git a/backend/src/entities/user-secret/use-cases/get-secret-audit-log.use.case.ts b/backend/src/entities/user-secret/use-cases/get-secret-audit-log.use.case.ts
index c9b32b194..b3b2138e3 100644
--- a/backend/src/entities/user-secret/use-cases/get-secret-audit-log.use.case.ts
+++ b/backend/src/entities/user-secret/use-cases/get-secret-audit-log.use.case.ts
@@ -46,9 +46,9 @@ export class GetSecretAuditLogUseCase
       data: logs.map((log) => buildAuditLogEntryDS(log)),
       pagination: {
         total,
-        page,
-        limit,
-        totalPages: Math.ceil(total / limit),
+        currentPage: page,
+        perPage: limit,
+        lastPage: Math.ceil(total / limit),
       },
     };
   }
diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
index 11a92fefa..50c95cd10 100644
--- a/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
+++ b/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
@@ -485,8 +485,6 @@ test.serial(`${currentTest}?page=1&limit=2 - should paginate audit log`, async (
     .set('Content-Type', 'application/json')
     .set('Accept', 'application/json');
 
-  const connectionId = JSON.parse(createdConnection.text).id;
-
   const response = await request(app.getHttpServer())
     .get('/secrets/test-api-key/audit-log?page=1&limit=2')
     .set('Cookie', token)

From fd852998fb4e22b071dfb888d6e7fb1354ce5b92 Mon Sep 17 00:00:00 2001
From: Andrii Kostenko 
Date: Thu, 4 Dec 2025 15:27:41 +0000
Subject: [PATCH 23/23] pagination test fix

---
 .../test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
index 50c95cd10..29d2f6b69 100644
--- a/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
+++ b/backend/test/ava-tests/non-saas-tests/non-saas-secrets-e2e.test.ts
@@ -493,7 +493,7 @@ test.serial(`${currentTest}?page=1&limit=2 - should paginate audit log`, async (
 
   t.is(response.status, 200, response.text);
   const responseBody = JSON.parse(response.text);
-  t.is(responseBody.pagination.perPage, 2);
+  t.is(responseBody.pagination.perPage, '2');
   t.true(responseBody.data.length <= 2);
 });