Server & Backend›5-Layer Architecture
5-Layer Backend Architecture
agentsecrets-server enforces a strict 5-Layer Asynchronous Architecture built on Django 5 and Django Ninja Extra. Every request flows through decoupled boundaries designed for high throughput, atomic consistency, and zero business logic inside HTTP controllers.
The 5-Layer Stack
┌─────────────────────────────────────────┐ │ HTTP Request (TLS) │ └────────────────────┬────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ 1. Thin API Controllers │ │ (Route parsing, Auth, HTTP mapping) │ └────────────────────┬────────────────────┘ │ ┌──────────────────┴──────────────────┐ ▼ ▼ ┌───────────────────────┐ ┌───────────────────────┐ │ 2. Query Selectors │ │ 3. Domain Services │ │ (Pure Reads, Caching) │ │ (Mutations & Atomicity)│ └───────────┬───────────┘ └───────────┬───────────┘ │ │ └──────────────────┬──────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ 4. Strict Pydantic Schemas │ │ (Input validation, extra="forbid") │ └────────────────────┬────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ 5. Django ORM Models │ │ (PostgreSQL / Temporal Pinning) │ └─────────────────────────────────────────┘
Layer Responsibilities and Invariants
1Controllers ()
Controllers serve exclusively as HTTP transport adapters:
- Scope: Route registration, header inspection, query parameter parsing, and status code mapping.
- Invariant: Maximum 25 lines of code per endpoint. Zero database ORM queries. Controllers never import Django models directly; they delegate 100% of data access to Selectors and Services.
- Output: All controllers return standardized
DataResponse[T]orErrorResponseenvelope structures.
@api_controller("/secrets", tags=["Secrets"]) class SecretsController: @http_post("/", response={200: DataResponse[BulkSecretOut], 400: ErrorResponse}) def bulk_upsert(self, request, payload: BulkSecretIn): result = SecretService.bulk_upsert_secrets( user=request.auth, project_id=payload.project_id, environment=payload.environment, secrets=payload.secrets ) return 200, CustomResponse.success(result)
2Query Selectors ()
Selectors contain all read-only database operations:
- Scope: Complex database filtering,
select_relatedandprefetch_relatedjoins, queryset aggregations, and cache lookups. - Invariant: Pure reads with zero mutations. Selectors are idempotent and safe to execute concurrently. They never call
.save(),.delete(), or.update().
class SecretSelector: @staticmethod def get_project_secrets(project_id: UUID, environment: str) -> list[SecretOut]: return list( Secret.objects.filter(project_id=project_id, environment=environment) .values("id", "key", "created_at", "updated_at") )
3Domain Services ()
Services contain all business operations and state mutations:
- Scope: State transitions, cryptographic derivation, password hashing, and multi-model workflows.
- Invariant: Atomic execution. All service mutations are wrapped in
with transaction.atomic()to prevent partial state corruption on failure. - Error Handling: Services raise typed domain exceptions (
RequestError,ResourceNotFound) rather than raw database or generic Python exceptions.
class SecretService: @staticmethod def bulk_upsert_secrets(user: User, project_id: UUID, environment: str, secrets: list[SecretEntry]) -> dict: with transaction.atomic(): # Validate workspace access WorkspaceSelector.require_write_access(user, project_id) # Perform double-envelope encryption & persistence created, updated = 0, 0 for item in secrets: obj, is_created = Secret.objects.update_or_create( project_id=project_id, environment=environment, key=item.key.upper(), defaults={"value": encrypt_fernet(item.value), "policy": item.policy} ) if is_created: created += 1 else: updated += 1 return {"created": created, "updated": updated}
4Schemas ()
Schemas define typed data contracts using Pydantic v2:
- Scope: Inbound payload validation, outbound response serialization, and type conversion.
- Invariant: Configured with
model_config = ConfigDict(extra="forbid")to reject unvalidated fields and prevent parameter injection attacks.
5Models ()
Models define PostgreSQL schema tables and relational constraints:
- Scope: Primary key definitions (UUIDv4), composite unique constraints (
(project_id, environment, key)), foreign key cascades, and temporal indexing. - Invariant: No columns for plaintext secret values. Secret ciphertext and policies are stored in typed
TextFieldandJSONFieldcolumns.
Double-Envelope At-Rest Encryption
[ Developer Machine ] [ agentsecrets-server Database ] Plaintext Secret (sk_live_...) │ ▼ (Client AES-256-GCM) Client Ciphertext (Opaque) │ ▼ (Network Payload) Received by Server ──────────────────────> Outer Fernet Layer (ENCRYPTION_KEY) └── Inner Client Ciphertext (AES-256-GCM)
When agentsecrets-server receives a secret payload:
- The incoming value is already encrypted ciphertext produced locally by the
agentsecretsCLI using AES-256-GCM. - The server encrypts this opaque blob with its server-level Fernet
ENCRYPTION_KEYbefore writing to PostgreSQL. - When serving secrets back to an authorized client, the server decrypts only its outer Fernet layer and returns the client-encrypted ciphertext. The server never possesses the client's private keys or plaintext values.
Was this helpful?
Thanks for your feedback!
Your feedback helps us improve the platform.