arche
The opinionated backend foundation for Axum applications.
Cloud integrations, databases, auth, LLM inference, tool-calling agents, encryption, streaming JSON/CSV, WebSockets, and structured error handling — wired up and ready to go.
arche sits around Axum, not in place of it.
Getting Started · Modules · API Reference · Design Principles
Why arche?
Every backend service re-implements the same infrastructure plumbing — cloud SDK setup, database pools, auth primitives, error handling, config resolution. arche bundles these into a single, cohesive Rust crate built on well-established libraries so you can skip the boilerplate and focus on business logic.
Getting Started
Add arche to your Cargo.toml:
[]
= "4.10.0"
Modules
| Module | What it does |
|---|---|
aws |
S3, SES, KMS, and CloudFront via official AWS SDKs |
gcp |
Generic GCP REST client + Vertex AI (Gemini + Claude); wrappers for Sheets, Drive, Cloud KMS, Cloud Storage, Cloud CDN, and Google OAuth login |
oidc |
OpenID Connect both ways — "Sign in with Google" client + build-your-own identity provider (authorization-code + PKCE, RS256) |
llm |
Canonical LLM types + LlmProvider trait — backend-agnostic |
agent |
Tool-calling agent engine, session state, SSE streaming |
database |
Postgres, Redis, and ClickHouse connection pooling with health checks |
jwt |
HS256 token generation, verification, and expiry helpers |
csv |
Async CSV read/write — batch, streaming, and from URL |
json |
Streaming JSON array parsing with metadata extraction |
crypto |
AES-128-CBC encryption with PBKDF2 key derivation |
sockets |
WebSocket connection registry with broadcast |
error |
Axum-compatible structured error responses (400–503) |
utils |
Alphanumeric nano IDs, timestamp validation, date/time conversions, pagination |
[!TIP] Every service module exports a config builder to wire up credentials programmatically — or omit it entirely (pass
None) and let arche resolve everything from environment variables.
// Pass None to resolve entirely from env vars
let pool = get_pg_pool.await?;
// Or configure explicitly
let config = default
.host
.port
.build;
let pool = get_pg_pool.await?;
All components are modular and explicit — nothing is hidden or magical.
API Reference
AWS
AWS SDK integrations built on official SDKs. Default region: ap-south-1.
S3
use ;
// From env vars
let client = get_s3_client.await?;
// Or with explicit config
let config = default
.credential_source
.access_key_id
.secret_access_key
.build;
let client = get_s3_client.await?;
| Env Var | Description |
|---|---|
S3_CRED_SOURCE |
"IAM" (default) or "env" |
S3_ACCESS_KEY_ID |
Required when source is "env" |
S3_SECRET_ACCESS_KEY |
Required when source is "env" |
S3_REGION |
AWS region (default: ap-south-1) |
KMS
use KMSClient;
// Default region
let kms = new_with_region.await;
// Encrypt / decrypt
let ciphertext = kms.encrypt.await?;
let plaintext = kms.decrypt.await?;
// Decrypt base64-encoded ciphertext directly
let plaintext = kms.decrypt_base64.await?;
| Env Var | Description |
|---|---|
AWS_REGION |
AWS region (default: ap-south-1) |
SES
use SESClient;
let ses = new_with_region.await;
// Plain email (with optional HTML body)
let message_id = ses.send_email.await?;
// Templated email
let message_id = ses.send_templated_email.await?;
| Env Var | Description |
|---|---|
AWS_REGION |
AWS region (default: ap-south-1) |
CloudFront
use ;
let aws = get_cloudfront_client.await;
let cf = new;
Invalidate paths — submits a CloudFront invalidation and returns immediately
with the invalidation ID and status (typically "InProgress").
let result = cf.invalidate_paths.await?;
println!;
Per CloudFront limits: paths must start with /, max 3000 paths per call,
caller reference ≤ 128 chars.
[!NOTE]
caller_reference: Noneauto-generates a fresh nanoid on every call, so a retried request creates a duplicate invalidation. Pass a stable value for idempotent retries.
Get invalidation status — fetch the current status of a previously created
invalidation. Returns the same InvalidationResult shape; status transitions
from "InProgress" to "Completed" (typically 5–15 minutes).
let status = cf.get_invalidation.await?;
println!;
Default distribution ID — set once on the client (or via
CLOUDFRONT_DISTRIBUTION_ID env) so per-call distribution_id can be None:
let config = default
.distribution_id
.build;
let cf = new;
cf.invalidate_paths.await?;
cf.get_invalidation.await?;
| Env Var | Description |
|---|---|
AWS_REGION |
AWS region (default: ap-south-1) |
CLOUDFRONT_DISTRIBUTION_ID |
Optional default distribution ID |
GCP
Service-account-authenticated REST client for any Google Cloud API, plus
ergonomic wrappers for Sheets, Drive, and Vertex AI. Built on reqwest —
honors HTTPS_PROXY / NO_PROXY like everything else.
Service account credentials
ServiceAccountKey is the canonical credential type. Two ways to construct:
use ServiceAccountKey;
// From individual fields (e.g. separate env vars or a secrets manager)
let key = new;
// Or from a GCP service-account JSON file on disk
let key = from_path.await?;
\n literals from .env-style storage are normalized to real newlines
automatically. The private_key is never readable back from the struct and
is masked in Debug output.
Authentication modes
Every GcpClient constructor accepts credentials in three forms, tried in
order:
- Explicit
ServiceAccountKey—GcpClient::new(Some(key), None, scopes). - Path to a service-account JSON file —
GcpClient::new(None, Some(path), scopes). - Neither → GKE / GCE metadata server (Workload Identity). When both are
None, arche falls back to${GCP_METADATA_URL:-http://metadata.google.internal}and exchanges the pod's bound service account for an OAuth token. This is the standard no-secrets-on-disk flow for pods running on GKE, Cloud Run, or GCE.
// Workload Identity — no creds passed in, no env vars required on GKE.
let kms = get_kms_client.await?;
Tokens are cached with a 60 s safety margin, single-flighted per scope set, and retried once on transient failures — uniformly across all three modes.
[!WARNING] Caveats for the metadata-server path:
- Scopes are ignored — the endpoint returns whatever scopes the pod's bound service account has. Need narrower scopes? Use an explicit
ServiceAccountKey.GCP_METADATA_URLis read once, at construction. Changing it later has no effect — set it before the firstGcpClient::new(None, None, …)call (handy for pointing tests at a mock).- Signed URLs are unsupported — V4 signing (
GcsClient::sign_*) needs the SA's private key, which the metadata server never exposes. Those calls return a clear error here.
Sheets
let sheets = client.await?;
let resp = sheets
.get
.await?
.send
.await?;
[!NOTE] Pass either
Some(key)orSome(path)— never both. Scope is preset tohttps://www.googleapis.com/auth/spreadsheets.
Drive
let drive = client.await?;
let bytes = drive
.get
.await?
.send.await?
.bytes.await?;
Scope is preset to https://www.googleapis.com/auth/drive.
Cloud KMS
Encrypt and decrypt against Google Cloud KMS using the same service-account
JWT auth as the rest of the GCP family — token caching, retries, and
concurrent-fetch deduplication come for free via GcpClient.
use ;
// Build the client. Any unset field falls back to its GCP_KMS_* env var.
let kms = get_kms_client.await?;
// Or fully env-driven (project_id required; location defaults to "global"):
let kms = get_kms_client.await?;
// Key identifier — passed per call so one client can target multiple keys
let kms_key = new;
// or: let kms_key = GcpKmsKey::from_env()?; // GCP_KMS_KEY_RING + GCP_KMS_KEY_NAME
// Encrypt — returns ciphertext + the key version that wrapped it
let out = kms.encrypt.await?;
// out.ciphertext: Vec<u8>
// out.key_version: e.g. "projects/.../cryptoKeys/my-key/cryptoKeyVersions/3"
// Persist this alongside the ciphertext for key-rotation auditing.
let plaintext = kms.decrypt.await?;
// If the ciphertext is already base64 (e.g. read from a DB column):
let plaintext = kms.decrypt_base64.await?;
| Env Var | Description |
|---|---|
GCP_KMS_PROJECT_ID |
GCP project hosting the KMS key (required) |
GCP_KMS_LOCATION |
KMS location (default: global) |
GCP_KMS_BASE_URL |
Override the Cloud KMS endpoint (testing / VPC-SC) |
GCP_KMS_KEY_RING |
Used by GcpKmsKey::from_env() |
GCP_KMS_KEY_NAME |
Used by GcpKmsKey::from_env() |
Already have a GcpClient configured for other services? Reuse it via the
exported scope — keeps a single token cache across Sheets / Drive / KMS:
let kms_gcp = my_gcp_client.with_scopes;
let kms = new;
Cloud Storage (GCS)
Object upload / download / delete / list / head, plus V4-signed GET URLs. Bucket is per-call so one client can target many buckets.
[!WARNING] Uploads and downloads buffer the full object in memory — fine for reports and assets, but route large media through a streaming path instead.
use ;
use HashMap;
use Duration;
// All fields optional — gcs_base_url and signed-URL expiry default fine.
let gcs = get_gcs_client.await?;
// Upload with optional user metadata (e.g. `modified_by`)
let mut meta = new;
meta.insert;
let object = gcs.upload.await?;
// object.generation: Option<i64> — round-trippable into download/head/delete
// Read it back
let bytes = gcs.download.await?;
// Or a specific historical version (requires Object Versioning on the bucket)
let old = gcs.download.await?;
// Metadata-only fetch
let meta = gcs.head.await?;
// Merge-patch user metadata (existing keys overwritten, others untouched)
let mut update = new;
update.insert;
gcs.patch_metadata.await?;
// List a prefix; pass `versions: true` to include non-current generations
let page = gcs.list.await?;
// V4-signed GET URL (defaults to client's expiry, max 7 days). Always
// points at `storage.googleapis.com` regardless of GCS_BASE_URL.
let url = gcs.signed_get_url?;
| Env Var | Description |
|---|---|
GCS_BASE_URL |
Override the storage endpoint (testing / VPC-SC; ignored for signed URLs) |
GCS_SIGNED_URL_EXPIRY_SECS |
Default expiry for signed_get_url (default: 900, max: 604800) |
upload switches automatically to multipart when called — user metadata
travels with the bytes in one request, no separate PATCH needed. Object names
containing / are URL-encoded for the JSON-API path but left literal in
V4-signed paths, matching GCS spec.
Cloud CDN
Cache invalidation against a global URL map, plus operation polling.
use ;
let cdn = get_cdn_client.await?;
// Invalidate. `path` must start with `/` and may use `*` as a suffix wildcard.
// `host` scopes the invalidation to a hostname routed by the URL map.
let op = cdn.invalidate.await?;
// op.name — the operation name; pass it to `invalidation_status` to poll
// op.status — "PENDING" / "RUNNING" / "DONE"
// op.progress — Option<i32>, 0..=100
// Poll until done
let status = cdn.invalidation_status.await?;
assert_eq!;
| Env Var | Description |
|---|---|
GCP_CDN_PROJECT_ID |
GCP project hosting the URL map (required) |
GCP_CDN_URL_MAP |
Optional default URL map name |
GCP_CDN_BASE_URL |
Override the Compute API endpoint |
Scope: global URL maps only — regional URL maps
(/regions/{region}/urlMaps/...) are not supported and will return 404.
Any other GCP REST API
GcpClient works for any Google API that accepts Authorization: Bearer …:
use GcpClient;
let pubsub = new.await?;
pubsub
.post
.await?
.json
.send.await?;
For full HTTP control (custom timeouts, TLS config, connection pool),
bring your own reqwest::Client:
let http = builder
.connect_timeout
.build?;
let storage = with_http.await?;
One service account, multiple APIs — share a single token cache:
let drive = client.await?;
let sheets = drive.with_scopes;
let storage = drive.with_scopes;
Vertex AI
VertexClient implements arche::llm::LlmProvider for Gemini and
Anthropic Claude models on Google Cloud. The provider (Gemini or Anthropic) is
captured at construction; the model is specified per-request.
use ;
use ServiceAccountKey;
use ;
// Gemini via API key (resolved from VERTEX_API_KEY / GEMINI_API_KEY env)
let client = get_vertex_client.await?;
// Service-account auth (required for Anthropic, optional for Gemini)
let key = new;
let client = get_vertex_client.await?;
let request = new
.with_system
.with_max_tokens
.with_temperature;
// Non-streaming
let response = client.generate.await?;
println!;
// Streaming
use StreamExt;
let mut stream = client.stream_generate.await?;
while let Some = stream.next.await
Function calling (typed schemas via arche::llm::ParameterSchema):
use ;
let tools = vec!;
let request = new
.with_tools;
Authentication:
| Method | When | Source |
|---|---|---|
| API Key | Gemini only | VertexConfig::with_api_key(...) or VERTEX_API_KEY / GEMINI_API_KEY env |
| Service Account | Gemini + Anthropic | VertexConfig::with_service_account_key(ServiceAccountKey) or with_service_account_key_path("/path/to/sa.json") |
[!NOTE] If an API key is present it takes priority; Anthropic models require service-account auth.
VERTEX_PROJECT_ID/VERTEX_REGIONoverride config (default regionasia-south1). Service-account creds must be passed viaVertexConfig— arche does not auto-resolveGOOGLE_APPLICATION_CREDENTIALS.
Token cache — every GCP REST call goes through a process-local token
cache: JWT-bearer flow against oauth2.googleapis.com/token, signed RS256
with the service-account key, retried once on transient failures, refreshed
60 s before expiry, single-flighted per (client_email, scopes) pair.
OIDC
Both halves of OpenID Connect — speak to a provider, or be one.
arche::oidc (client) |
arche::oidc::server |
|
|---|---|---|
| Role | Relying party — "Sign in with Google" | Identity provider — "Sign in with your service" |
| You get | authorize URL → code → verified ID token | validate → single-use code → signed ID token |
| You bring | provider metadata + your config | four small trait impls (registry · signer · tokens · store) |
Authorization-code flow with mandatory PKCE (S256), RS256 ID tokens, and a JWKS cache that handles key rotation transparently. The two halves never share code — an interop test drives the client against the server over real HTTP to prove they speak the same dialect.
[!TIP] Full walkthrough, both sequence diagrams, and a single hover-tooltip canvas live in
docs/oidc/.
Client — "Sign in with Google" (or any OIDC provider)
Provider endpoints come from a ProviderMetadata: the shipped google() preset,
a struct literal, or ProviderMetadata::discover(...) (fetches
/.well-known/openid-configuration).
use google;
use ;
// Build once, hold on state — both are Clone.
let client = new?;
let verifier = new?;
// 1 ─ send the user to the provider's consent screen
let url = client.auth_url;
// 2 ─ on callback, trade the code for tokens
let tokens = client.exchange_code.await?;
// 3 ─ verify the ID token into YOUR claims type
let claims: Claims = verifier
.verify_id_token
.await?;
[!NOTE]
verify_id_token::<C>validates RS256, the signingkid(against a rotation-aware JWKS cache), the signature, andiss/aud/exp/iat/nbf(60 s skew) — then deserializes into yourC. A bad token isAppError::Unauthorized(the real reason is logged at debug, never leaked); an unreachable provider isAppError::DependencyFailed; a valid token that doesn't fitCis an internal error, not a 401. Nonce is yours to check — put it inCand compare.
Server — be your own identity provider
arche runs the protocol logic; it does not serve HTTP. You wire four routes and call four methods. The server is generic over four capabilities — arche ships a built-in only where correctness is math, never policy:
| Capability | Trait | Built-in | Bring your own when… |
|---|---|---|---|
| Client resolution | ClientRegistry |
— always yours | static list · DB · config service (override verify_secret for hashed secrets) |
| Token signing | TokenSigner |
SigningKey (local RSA) |
keys live in KMS / HSM, or you rotate |
| Access tokens | AccessTokenIssuer |
— always yours | opaque random · your own JWT · a stored token |
| Code storage | CodeStore |
— always yours | in-memory (single node) · Redis GETDEL · PG DELETE…RETURNING |
use AppError;
use ;
use HashMap;
use ;
use Mutex;
// Signer: a local RSA key is the one built-in. Persist it — a fresh key
// invalidates every issued ID token. openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048
let key = from_pem?;
// Registry: resolve partner apps. `verify_secret` is defaulted (constant-time
// plaintext); override it when you store hashed secrets.
;
// Access token: opaque noise is fine when only the ID token is consumed;
// mint a real JWT when something must verify it.
;
// Code store: `take` MUST be atomic delete-on-read (the single-use guarantee).
// In-memory works on one node; behind a load balancer use Redis GETDEL / PG.
>>);
2 · Build the server — once, at startup:
use ;
let server = new?;
3 · Wire four routes — each is one method call:
use ;
// GET /.well-known/openid-configuration
let mut doc = standard; // all fields pub — override to taste
Json
// GET /jwks (send Cache-Control: public, max-age=3600)
Json
// GET /authorize
let validated = match server.validate_authorize.await ;
match session_user
// POST /token (send Cache-Control: no-store on success)
let req = TokenRequest ;
match server.exchange.await
[!IMPORTANT] One instance, cloned per request — never one-per-request. Build the
OidcServeronce at startup and keep it on state;.clone()is a single refcount bump. This isn't just efficiency: theCodeStorespans two requests —/authorizecallsput(code), and/token(a different request, seconds later) callstake(code). A server rebuilt per request hands the second call an empty store, and every login fails withinvalid_grant.
[!TIP] Keep it isolated: hold the server in its own service state, reached through the
Stateextractor — no shared god-object, no middleware required. Give each service its own state and compose routers; reserve shared state for truly-global primitives (DB pools, secrets).
Claims are yours; protocol claims are arche's. issue_code(request, subject, claims)
takes the sub (1–255 ASCII) and any Serialize claims and mints them verbatim —
except the reserved set (iss, sub, aud, iat, exp, nbf, nonce, jti,
azp, at_hash, c_hash), which arche strips from your input and stamps itself.
auth_time is deliberately not reserved — you assert it (and must, to honor a
max_age request). To resume after a login page, round-trip the serializable
ValidatedAuthorizeRequest back into issue_code; it re-checks client_id /
redirect_uri against the registry, so a tampered stash is rejected, not used.
[!WARNING] Not supported, by design: refresh tokens, the client-credentials grant, and
/userinfo(claims ride in the ID token). Key rotation is aTokenSignerchoice, not a limitation.
Deeper reading:
docs/oidc/README.md— index for both halvesdocs/oidc/architecture.md— the two halves, component diagram, four seams, where each defense livesdocs/oidc/sequence.md— login flow both directions, whatstate/ PKCE defend, error + wire tablesdocs/oidc/extending.md— client & server quickstarts, code for each of the four traits
LLM
Canonical, provider-agnostic types and the LlmProvider trait that every backend
implements. Use it directly when you just want to call an LLM; build on top of it
when you want tool-calling orchestration (see agent).
use ;
// `client` is anything implementing `LlmProvider` —
// VertexClient, or your own OpenAi/Bedrock/Ollama/local impl.
let request = new
.with_system
.with_temperature;
let response = client.generate.await?;
Types you'll use:
| Type | Purpose |
|---|---|
LlmProvider (trait) |
generate() + stream_generate() on a canonical GenerateRequest. Implement this to add a backend. |
GenerateRequest / GenerateResponse |
Canonical request/response, provider-neutral |
Message, Role, ContentPart |
Conversation turns — text, tool calls, tool results |
StreamChunk |
Text(String) | ToolCall { id, name, arguments } | Done { finish_reason, usage } |
ToolDefinition + ParameterSchema |
Strictly-typed tool descriptions; serializes to valid JSON Schema |
Usage |
Token accounting (input/output/total) |
Writing a custom backend:
use ;
use AppError;
use Future;
use Pin;
Drops into arche::agent::get_agent_engine(my_client, config) with no other changes.
Agent
Tool-calling agent engine: orchestrates LLM rounds, invokes your tools, streams SSE events to the client, manages session history (with optional compaction).
use ;
use ;
use ;
;
// Wire it up
let client = get_vertex_client.await?;
let config = builder.build?;
let engine = get_agent_engine
.with_default_summarizer; // optional, cheap summarization
// Per request
let mut session = new;
let stream = engine.run;
// Map each SseEvent via `to_sse_event(..)` to an axum SSE Event.
What arche provides vs. what you write:
| Arche provides | You write |
|---|---|
| Orchestration loop, streaming, SSE event types, session mutation, tool-calling loop, history compaction | System prompt, tool schemas, tool executors (impl AgentFlow), HTTP handler, session persistence |
Extension points:
| Need | Plug point |
|---|---|
| Different LLM backend | impl LlmProvider for YourClient |
| Custom history compaction (vector recall, server-side memory) | impl HistoryCompactor |
| Custom UI events from tools | ToolOutput::text(..).data(type, payload) → reaches client via SseEvent::Data |
Deeper reading:
docs/agent/architecture.md— module layering, component diagram with hover tooltipsdocs/agent/sequence.md— request lifecycle, error paths, SSE wire formatdocs/agent/extending.md— step-by-step guides for each plug point
Database
Postgres
Connection pooling with sqlx, configurable credentials, and health checks.
use ;
let pool = get_pg_pool.await?;
let is_healthy = test_pg.await?;
| Env Var | Description |
|---|---|
PG_HOST |
Database host |
PG_PORT |
Database port |
PG_DATABASE |
Database name |
PG_MAX_CONN |
Maximum pool connections |
PG_USERNAME |
Username |
PG_PASSWORD |
Password |
PG_CREDENTIALS |
JSON string {"username":"...","password":"..."} (alternative to separate vars) |
Redis
Connection pooling with bb8, optional password auth, and health checks.
use ;
let pool = get_redis_pool.await?;
let is_healthy = test_redis.await?;
| Env Var | Description |
|---|---|
REDIS_HOST |
Redis host |
REDIS_PORT |
Redis port |
REDIS_MAX_CONN |
Maximum pool connections |
REDIS_PASSWORD |
Optional password |
ClickHouse
Read-only connection pooling with bb8 (round-robin across replicas) and a
typed row API. SQL templates are &'static str — a compile-time check that
prevents user input from being concatenated into a query.
use ;
let pool = get_clickhouse_pool.await?;
let conn = pool.get_conn.await?;
let counts: = conn
.query
.bind
.fetch_all.await?;
Notes:
- Bare
SELECT */SELECT t.*are blocked. Call.allow_select_star()on a query, set.allow_select_star(true)on the config, or setCLICKHOUSE_ALLOW_SELECT_STAR=trueto bypass. - Writes go through Kafka → Kafka Connect ClickHouse Sink, not this connector.
[!WARNING]
query/executetake&'static stron purpose — user input can't be concatenated into the SQL. Runtime-built SQL is still possible viaquery_dynamic(String)/execute_dynamic(String), but those shift injection-safety onto you.
| Env Var | Description | Default |
|---|---|---|
CLICKHOUSE_HOSTS |
Comma-separated replica hostnames | — (required) |
CLICKHOUSE_HOST |
Single-host fallback if CLICKHOUSE_HOSTS is unset |
— |
CLICKHOUSE_PORT |
Server port | 8443 (secure) / 8123 (plain) |
CLICKHOUSE_DATABASE |
Default database | default |
CLICKHOUSE_USERNAME |
Username | default |
CLICKHOUSE_PASSWORD |
Password | (empty) |
CLICKHOUSE_SECURE |
HTTPS toggle | true |
CLICKHOUSE_MAX_POOL_SIZE |
Max pool connections | 32 |
CLICKHOUSE_CONNECTION_TIMEOUT_MS |
Pool-acquire timeout | 5000 |
CLICKHOUSE_REQUEST_TIMEOUT_MS |
Per-request max_execution_time |
30000 |
CLICKHOUSE_COMPRESSION |
lz4 or none |
none |
CLICKHOUSE_ALLOW_SELECT_STAR |
Global SELECT * escape hatch |
false |
JWT
Token generation and verification using HS256.
[!NOTE] This is symmetric HS256 for your app's own access / refresh tokens. For asymmetric RS256 ID tokens in an OIDC flow (signing or verifying "Sign in with…" tokens), see
oidc— different keys, different purpose.
use ;
use ;
// Generate an access + refresh token pair
let tokens = generate_tokens?;
// Verify a token
let data = ?;
CSV
Async CSV processing powered by csv-async. Supports reading from bytes, files, and
URLs — with both batch and streaming modes.
use CsvClient;
// Default config (comma-delimited, with headers)
let csv = new;
// Or customize
let csv = new
.delimiter
.has_headers
.flexible;
Reading
use Deserialize;
// From bytes
let records: = csv.read.from_bytes.deserialize.await?;
// From file
let records: = csv.read.from_file.deserialize.await?;
// From URL
let records: = csv.read.from_url
.deserialize.await?;
// Batch processing (memory-efficient for large files)
csv.read.from_file
.deserialize_batched.await?;
Writing
use Serialize;
let records = vec!;
// To bytes
let bytes: = csv.write_all.await?;
// To file
csv.write_file.await?;
Streaming
// Record-by-record reading
let mut stream = csv.read.from_file.stream.await?;
while let Some = stream..await
// Record-by-record writing
let mut writer = csv.writer_to_file.await?;
writer.serialize.await?;
writer.finish.await?;
JSON
Streaming JSON array parsing optimized for large payloads. Extracts metadata fields before the target array and streams array elements one-by-one or in batches — without loading the full document into memory.
use JsonClient;
use Deserialize;
let json = new;
// Stream a root-level JSON array from bytes
let source = json.from_bytes;
let mut stream = source.stream_root_array;
while let Some = stream..await
// Stream a nested array with metadata capture
// Given: {"total": 1000, "items": [{...}, {...}, ...]}
let json = new;
let source = json.from_bytes;
let mut stream = source.stream_array.await;
while let Some = stream..await
let total: u64 = stream.field?;
// Batch iteration
let batch = stream..await;
// Stream directly from S3
let source = new.from_s3.await?;
let mut stream = source.stream_array.await;
Crypto
AES-128-CBC encryption with PBKDF2-HMAC-SHA1 key derivation (65,536 iterations).
[!NOTE] The
saltmust be ≥ 16 bytes. This is a fixed CBC + PBKDF2-SHA1 scheme; when you own both ends and want authenticated encryption, an AEAD cipher (AES-GCM) is the stronger default.
use ;
let secret = "my-secret-key";
let salt = "my-salt-value-16"; // minimum 16 bytes
// Encrypt — returns raw ciphertext bytes
let ciphertext = encrypt_cbc?;
// Decrypt — expects base64-encoded ciphertext input
let plaintext = decrypt_cbc?;
Sockets
WebSocket connection registry with broadcast support. Manages a thread-safe map of active connections for fan-out messaging.
use SocketConnectionManager;
let manager = new;
// Register a connection (typically in a WebSocket upgrade handler)
manager.add?;
// Broadcast to all connected clients
manager.broadcast?;
// List active connections
let ids = manager.get_connections?;
// Remove a connection on disconnect
manager.remove?;
Error
Axum-compatible structured error handling. Every variant converts to a JSON response with the appropriate HTTP status code.
use AppError;
async
Variants:
| Variant | Status | Constructor |
|---|---|---|
BadRequest |
400 | AppError::bad_request(errors, message, description) |
Unauthorized |
401 | Direct construction |
Forbidden |
403 | Direct construction |
NotFound |
404 | AppError::not_found("resource") |
Conflict |
409 | AppError::conflict("message") |
UnprocessableEntity |
422 | AppError::unprocessable_entity(errors, message, description) |
DependencyFailed |
424 | AppError::dependency_failed("upstream", "detail") |
InternalError |
500 | AppError::internal_error(error, message) |
Unavailable |
503 | Direct construction |
InternalError responses are sanitized by default — no leaked SQL or infra
details.
[!WARNING] The
verbose-errorsfeature echoes raw error details into responses. Enable it in dev / staging only — never in production.
= { = "4.10.0", = ["verbose-errors"] }
Utils
ID generation, date/time conversion traits, and pagination helpers.
Nano IDs
URL-safe, strictly alphanumeric unique IDs (0-9 a-z A-Z — no - or _),
so they're safe in subdomains, file names, and anywhere symbol characters
cause friction:
use ;
// 21 characters — same collision resistance class as a standard nanoid
let id = nano_id; // e.g. "V1StGXR8Z5jdHi6BmyT9k"
// Custom length
let short = nano_id_of; // e.g. "fX3kQ9aZ"
Date/time & pagination
use ;
use OffsetDateTime;
// Check if a timestamp is in the future
let is_valid = validate_timestamp?;
// Convert OffsetDateTime to ISO string
let iso = offset_dt.to_iso_string?;
// Pagination query params (for Axum extractors)
let params = PaginationParams ;
Re-exported Dependencies
arche re-exports these crates so you don't need to add them separately:
axum · tokio · serde · serde_json · sqlx · time · tracing · tracing-subscriber · reqwest · jsonwebtoken · nanoid · thiserror · base64 · bb8 · bb8-redis · clickhouse (as ch_client) · csv-async · futures · tokio-stream · dotenv · aws-config · aws-sdk-s3 · aws-sdk-sesv2 · aws-sdk-kms · aws-sdk-cloudfront
Design Principles
- Explicit over implicit — no hidden global state or magic
- Composition over inheritance — thin wrappers you combine as needed
- Production-first defaults — sane defaults, sanitized errors, pooled connections
- Async-native — built on Tokio from the ground up
What arche is not
- A framework that replaces Axum
- A code generator or project template
- A monolithic abstraction over third-party libraries