oshrin/
├── api/ Express + ts-rest backend
├── web/ React + Vite frontend (served via nginx in production)
├── shared/ Zod schemas, ts-rest contracts, shared types
shared is consumed by both api and web as @oshrin/shared via pnpm workspace.
User
└── Project (created by user)
├── Environment (development, staging, production, ...)
│ └── Secret (key + AES-256-GCM encrypted value)
├── ProjectMember (shared access: editor or viewer)
└── ProjectInvite (pending invite with token + expiry)
- Project ownership is tracked via
projects.createdBy. - Project slugs are unique per user (
createdBy + slug). - Cascading deletes: User deletion cascades through Project → Environment → Secret. Activity log entries for deleted projects/environments are set to null (preserved for audit).
All checks live in authorization.service.ts:
| Method | Who passes | Used for |
|---|---|---|
assertProjectAccess |
Owner or any member | Read operations on project/environments |
assertProjectOwner |
Owner only | Delete project, manage invites/members |
assertProjectWriteAccess |
Owner or editor | Create/update environments |
assertEnvironmentWriteAccess |
Owner or editor | Create/update/delete secrets |
Viewers can read everything but cannot modify environments or secrets.
Dual-mode JWT authentication in auth.middleware.ts:
- Cookie mode (web UI):
access_tokencookie + CSRF token validation on state-changing requests. CSRF comparison usescrypto.timingSafeEqual. - Header mode (API clients):
Authorization: Bearer <token>. No CSRF needed.
Token lifecycle:
- Access token: Short-lived JWT with
userId,email,tokenVersion - Refresh token: Longer-lived JWT with
userId,tokenVersion - Token versioning:
users.tokenVersionis incremented on password change or "logout all". Any token with an old version is rejected.
Secret values are encrypted at rest using AES-256-GCM (utils/encryption.ts):
- Generate random 64-byte salt + 16-byte IV per secret
- Derive key from
ENCRYPTION_KEYvia PBKDF2 (100k iterations, SHA-512) - Encrypt with AES-256-GCM, store as base64:
salt | iv | authTag | ciphertext
This is not a zero-knowledge model — the server can decrypt secrets. See SECURITY.md for tradeoffs.
ts-rest contract (shared/) → router (api/src/routes/) → service → repository → Drizzle ORM → PostgreSQL
- Contracts (
shared/contracts/): Define routes, request/response schemas, and HTTP methods. Shared between frontend and backend. - Routers (
api/src/routes/): Bind contracts to handlers, applyauthenticatemiddleware. - Services (
api/src/services/): Business logic, authorization checks, encryption/decryption. - Repositories (
api/src/repositories/): Reusable database queries via Drizzle ORM.
- No zero-knowledge encryption. The server derives encryption keys from a master key. This allows server-side search and export at the cost of requiring trust in the server operator.
- Resource limits are configurable. All limits default to reasonable values but can be disabled (
0) for self-hosted instances. - Transactions are used sparingly. Only for multi-write operations: email verification, password reset, invite acceptance.