Skip to main content
ClaudeWave
Skill4.2k repo starsupdated 3d ago

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.

Install in Claude Code
Copy
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-architecture
Then start a new Claude Code session; the skill loads automatically.

SKILL.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 act