cloud-storage-hexagonal-architecture
Enforce hexagonal architecture in the Rust backend. Use before modifying crates/ or Rust services, especially inbound axum/tool/listener adapters, domain services/ports, outbound adapters, authorization, permissions, database access, or external clients.
git clone --depth 1 https://github.com/macro-inc/macro /tmp/cloud-storage-hexagonal-architecture && cp -r /tmp/cloud-storage-hexagonal-architecture/.claude/skills/cloud-storage-hexagonal-architecture ~/.claude/skills/cloud-storage-hexagonal-architectureSKILL.md
# Cloud Storage Hexagonal Architecture Guard
Use this skill whenever you add, change, or review Rust code under `crates/**` or `services/**` that touches a crate with `src/domain`, `src/inbound`, or `src/outbound`.
This repository follows the ports-and-adapters / hexagonal style described in _Master Hexagonal Architecture in Rust_ and the `howtocodeit/hexarch` `3-simple-service` branch: domain models + ports + services are the center; inbound and outbound adapters are replaceable shells around that center.
## Non-negotiable dependency rule
Dependencies point inward:
```text
inbound adapters ──► domain ports/models/services ◄── outbound adapters
composition root ──► inbound + domain service + outbound implementations
```
- `domain/` must not depend on `inbound/`, `outbound/`, `axum`, HTTP response types, SQLx pools/queries, AWS SDKs, Redis, reqwest, environment variables, or transport DTOs.
- `inbound/` may depend on domain ports/models/services. It must not own business decisions or persistence/external-service implementation details.
- `outbound/` implements domain ports for databases, S3, HTTP clients, queues, metrics, etc. It must not own use-case policy.
- Wiring concrete adapters into services belongs in the composition root / builder, not inside domain logic or handlers.
## Layer responsibilities
### Domain (`src/domain/**`)
Put the following here:
- Domain models, value objects, command/request types, response types, and domain errors.
- Service/use-case traits exposed to inbound adapters.
- Concrete domain service implementations that orchestrate a use case.
- Port traits for required capabilities: repositories, authorizers, notifiers, event publishers, clocks, ID generators, metrics, external domain services.
- Business invariants, state transitions, authorization policy, ownership checks, permission-level checks, tenant/team/workspace policy, filtering rules, and side-effect orchestration.
### Inbound adapters (`src/inbound/**`)
Axum handlers, AI tools, Kafka/listener handlers, lambda handlers, and CLI entrypoints are adapters. Keep them thin:
- Extract authentication/identity from transport (`MacroAuthorizationExtractor`, `OptionalMacroAuthorizationExtractor`, JWT, signed internal header, request context).
- Parse path/query/body/header data and perform transport/syntax validation.
- Convert transport DTOs into domain request/command types.
- Call exactly the appropriate domain service/port method.
- Convert domain success/errors into transport responses/status codes/tool output.
Inbound adapters must not:
- Decide whether a user may access/edit/delete/share/list an entity.
- Call `entity_access`, `roles_and_permissions`, repositories, SQLx, S3, Redis, SQS, reqwest, or other outbound implementations to make a use-case decision.
- Branch on `AccessLevel`, role, owner/admin/member, tenant/team membership, project membership, subscription tier, feature entitlement, entity state, or ownership for business policy.
- Filter returned entities by permissions or hide fields based on authz rules.
- Start transactions or compose multiple persistence/external calls as the core use case.
### Outbound adapters (`src/outbound/**`)
Put implementation details here:
- SQLx queries and transaction mechanics.
- AWS/Redis/OpenSearch/HTTP client calls.
- Mapping external errors to domain errors as required by a port contract.
- Implementing repository/authorizer/client/notifier ports.
Outbound adapters must not:
- Import `crate::inbound::*`, axum extractors/responses, or transport DTOs.
- Invent business policy beyond faithfully implementing the domain port contract.
- Decide use-case flow; return facts/capabilities/results for the domain service to decide.
## Authorization and `EntityAccessReceipt` rule
Authentication can happen at the edge. Entity access checks should cross the boundary as a typed capability: `EntityAccessReceipt<T>`.
`EntityAccessReceipt<T>` means the entity access layer has verified the caller has at least permission `T` for the entity. Inbound adapters may obtain this receipt through the standard access extractors or by calling the entity access service specifically to mint a receipt. After that, ordinary handlers/tools/listeners must pass the receipt inward instead of re-checking or branching on authorization.
Allowed in inbound:
- Reject missing/invalid credentials (`401` / unauthenticated).
- Extract `actor`, `request_context`, `user_id`, service identity, internal principal, or a typed `EntityAccessReceipt<T>`.
- Use standard access extractors or `generate_entity_access_receipt::<RequiredLevel>(...)` to mint a receipt.
- Pass the receipt and parsed request data into the domain service call.
- Convert domain/access errors into transport responses.
Forbidden in ordinary inbound handlers/tools/listeners:
- `if user_id != owner_id { ... }`
- `if access_level < Edit { ... }`
- `entity_access_service.get_access_level(...)` or `can_edit(...)` followed by allow/deny branching.
- Role/team/tenant/project permission checks.
- Inspecting `receipt.entity_permission()` to decide use-case business policy.
- Direct repository/persistence calls for the protected action.
Correct pattern:
1. Pick the minimum required permission type for the use case, e.g. `ViewAccessLevel`, `EditAccessLevel`, or `OwnerAccessLevel`.
2. In inbound, obtain `EntityAccessReceipt<RequiredLevel>` using the existing entity access boundary.
3. Pass that receipt to the domain service method.
4. In the domain service, perform use-case-specific policy that is not captured by the minimum receipt type, e.g. owner-only share changes inside an edit operation.
5. Return a domain error such as `Unauthorized`, `Forbidden`, or a typed policy error.
6. Let inbound map that domain/access error to HTTP/tool/listener semantics.
7. Unit-test allow and deny cases at the domain service level with fake receipts/ports.
## Bad vs good
Bad: the handler branches on permissions and performs the protected actBuild a new AI tool end-to-end — Rust implementation, toolset wiring, infra, schema generation, and frontend UI.
Find all open Dependabot alerts for this repo and create a plan to resolve them using the appropriate package manager overrides (pnpm, bun, npm, cargo).
Dump clean Postgres schema to a file and copy path to clipboard.
Quality gate. 5 parallel agents review changes. All must pass.
Upgrade an AI chat model (fast or good) across backend and frontend.
Launch local Swagger UI for a service's OpenAPI spec
Validate Rust work after substantial Rust code changes by running `just check`, `just clippy`, then `just format`. Use before the final response after a significant Rust implementation or cleanup task; batch edits first instead of running after every small change.