Introduction
Detritus is a lightweight crash and log receiver for small Rust projects.
Version 0.1.0 has two ingest paths:
- OTLP/gRPC logs through
opentelemetry.proto.collector.logs.v1.LogsService.Export - HTTP/2 multipart crash uploads at
/v1/crashes
The workspace is split into three crates:
detritus-protocolprovides the shared wire types, crash schema, and curated OTLP facade.detritus-clientprovides the tracing layer, panic hook, and offline crash shipper for Rust applications.detritus-serverprovides thedetritusdreceiver binary and an embeddable server API.
The documentation in this book focuses on the stable operational and architectural contract that is already implemented in the repository:
- the one-process, one-port receiver model
- filesystem-backed persistence
- per-project bearer-token auth
- payload-level schema validation
- zstd-compressed crash uploads with byte-exact server-side storage
- single-instance and multi-instance NixOS deployment
API reference for the published crates lives on docs.rs:
Installation
Detritus is a Rust workspace with a Nix flake. You can build it either with Cargo directly or through the flake outputs.
Prerequisites
- Rust
1.88or newer for direct Cargo builds protocavailable onPATHfor direct Cargo builds of the protocol crate- Nix with flakes enabled if you want the reproducible flake environment
Build With Nix
From the repository root:
nix develop
nix build .#detritus
nix build .#docs
nix develop gives you the workspace toolchain plus mdbook.
Build With Cargo
From the repository root:
cargo build --workspace --all-features
cargo test --workspace --all-features
To run the receiver binary from source:
cargo run -p detritus-server -- --help
Crate Installation
Operators can install the published receiver binary with Cargo:
cargo install detritus-server
Library users depend on the published crates in the usual Cargo way:
[dependencies]
detritus-client = "0.1.0"
detritus-protocol = "0.1.0"
Quick Start
This quick start brings up a local receiver and shows the minimum configuration the server expects.
1. Prepare a token file
detritusd requires a TOML file containing Argon2 PHC hashes, not raw bearer tokens. The file shape is:
[[token]]
id = "local-dev"
secret = "$argon2id$v=19$m=19456,t=2,p=1$..."
project = "detritus"
source_prefix = "detritus/"
[rate_limit]
logs_per_minute = 1000
logs_burst = 200
crashes_per_minute = 30
crashes_burst = 5
Every ingest request except /healthz and /metrics must present the matching bearer token with Authorization: Bearer <token>.
2. Start the receiver
From the repository root:
cargo run -p detritus-server -- \
--bind 127.0.0.1:4317 \
--data-dir ./detritus-data \
--tokens-config ./tokens.toml
The default listener is 127.0.0.1:4317. That single listener serves both OTLP/gRPC logs and multipart crash uploads.
3. Verify the local listener
curl -i http://127.0.0.1:4317/healthz
The handler returns 204 No Content on success.
4. Exercise crash ingest
curl -X POST \
-H "Authorization: Bearer <token>" \
http://127.0.0.1:4317/v1/crashes \
-F 'metadata={"schema_version":1,"source":{"project":"detritus","platform":"linux","version":"smoke","install_id":"00000000-0000-4000-8000-000000000001"},"timestamp":"2026-05-19T00:00:00Z","kind":"PanicTarball","build":{"git_sha":"smoke","profile":"release","target_triple":"x86_64-unknown-linux-gnu"},"panic_text":"smoke","context":{},"attachments":[]}' \
-F dump=@/tmp/fake.bin
On success the server responds with 201 Created and a JSON body containing the crash blob hash and whether the upload deduplicated against an existing blob.
5. Point a Rust application at the receiver
The client SDK installs as a tracing layer plus an optional panic hook.
The install_layer example enables zstd-compressed OTLP log exports to the local
Detritus 0.2 receiver with .compression(detritus::CompressionEncoding::Zstd).
See the crate examples for runnable code:
cargo run --example install_layer -p detritus-clientcargo run --example crash_envelope -p detritus-protocolcargo run --example embed_server -p detritus-server
Architecture
This document captures the implemented v1 architecture decisions.
Naming
The project name is detritus.
Workspace crates are:
detritus-protocoldetritus-clientdetritus-server
The server binary is detritusd. The client-facing Rust API is centered on detritus::Layer and detritus::install_panic_hook.
Logs
Logs use OTLP/gRPC. The server implements opentelemetry.proto.collector.logs.v1.LogsService.Export.
This keeps the log path aligned with standard OpenTelemetry clients while avoiding a custom log wire protocol.
Crash Uploads
Crash dumps use an HTTP/2 multipart POST to /v1/crashes.
Two multipart parts are required:
metadata, a JSONCrashMetadatadocumentdump, the binary crash artifact
Optional attachments use multipart names of the form attach:<key>.
One Process, One Port
Detritus runs as one process on one port. The receiver uses axum and tonic::routes() so gRPC and HTTP requests share one axum::Router.
That single listener is the deployment contract used by the local quick start, the NixOS module, and reverse-proxy configurations.
Persistence
Persistence is filesystem-only in v1.
- Logs are appended as NDJSON files per
(project, source-id, day). - Crash dumps and attachments are stored in a content-addressed SHA-256 blob store.
- Per-source crash indexes are stored as JSON alongside the source namespace.
The server does not require a database.
Auth
Auth uses bearer tokens in the Authorization header.
Each token is scoped to:
- one
project - one canonical
source_prefix
Server-side token files store Argon2 PHC hashes instead of raw bearer tokens.
Compression
Crash uploads use a two-layer compression model.
Wire-level gzip is handled by the HTTP/gRPC stack. Payload-level zstd is handled by detritus-client before upload. The server stores the uploaded bytes exactly as received and records per-part content_encoding in the on-disk crash index when a part was encoded.
See Compression Model for the concrete rules.
Multi-Tenancy
The top-level namespace is project. The client identity inside a project is install_id.
That shape is reused across:
- token authorization
- storage layout
- schema validation
- multi-instance deployment planning
Storage Layout
detritusd --data-dir <path> stores all ingest artifacts under that root:
<data-dir>/
logs/<project>/<source-id>/YYYY-MM-DD.ndjson
crashes/by-hash/<2-hex-prefix>/<sha256>.bin
crashes/by-source/<project>/<source-id>/<timestamp>-<sha256>.json
tmp/
<source-id> is the SourceId.install_id UUID. Project, platform, version, and install ID are still preserved inside crash metadata and the required OTLP resource attributes.
Logs
Logs are append-only NDJSON.
Each accepted OTLP LogRecord produces one JSON line in the current UTC day file for its source.
Crash Blobs
Crash dumps and attachments are content-addressed by SHA-256.
The server writes uploaded bytes into <data-dir>/tmp/, fsyncs the temporary file, links it into crashes/by-hash/<2-hex-prefix>/<sha256>.bin, and then removes the temporary file.
If the hash already exists, the upload is treated as a deduplicated blob and the temporary file is removed.
Crash Indexes
Each accepted crash upload also writes a per-source JSON index under:
crashes/by-source/<project>/<source-id>/
The index contains:
- the submitted
CrashMetadata - the dump blob pointer
- attachment blob pointers
content_encodingfor any part uploaded with an encoding such aszstd
Compression Model
Detritus uses two distinct compression layers for crash ingest.
Layer 1: Transport Compression
Transport compression is handled by the HTTP and gRPC stack.
- OTLP/gRPC accepts uncompressed, gzip, and zstd messages. The client sends
uncompressed requests by default; select an algorithm with
LayerBuilder::compression. Response compression is negotiated with Tonic. - Crash uploads accept whole-request gzip and zstd compression, independently of
multipart part encodings. HTTP responses negotiate gzip or zstd using
Accept-Encoding. - The 150 MiB HTTP request-body limit is applied after decompression.
For example, add .compression(detritus::CompressionEncoding::Zstd) to a log
layer builder when using an upgraded receiver. A failed compressed export is
spooled as the original protobuf request, so replay remains independent of the
transport encoding selected by the next process.
The log exporter reuses a Tonic channel across requests and applies the flush
timeout to both connection establishment and each RPC, including the gRPC
deadline sent to the receiver. HTTPS uses system trust roots; a
detritus::ClientTlsConfig supplied to LayerBuilder::tls_config supports private
CAs, domain overrides, and client identities for mutual TLS. TLS termination
remains at the reverse proxy in the standard receiver deployment.
These integrations use the documented Tonic compression API, Tonic TLS configuration, and tower-http decompression.
Layer 2: Payload Compression
detritus-client compresses crash payload bytes before upload.
- Dump bytes are compressed with zstd by default.
- The default zstd level is
19. - Text-like attachments are also compressed by default.
- The metadata JSON part is never compressed.
The client sets Content-Encoding: zstd on the individual multipart parts that were compressed.
Attachment Heuristic
The client compresses these content types:
text/*application/jsonapplication/x-ndjsonapplication/yamlapplication/x-yamlapplication/xml
The client leaves these content types uncompressed:
application/zstdapplication/gzipapplication/x-gzipapplication/zipapplication/x-tarapplication/octet-streamimage/*video/*audio/*
Hashing And Deduplication
The crash blob hash covers the bytes sent over the wire, which means the SHA-256 is computed over the compressed bytes.
That preserves dedup semantics: the same source dump compressed the same way lands on the same content-addressed blob.
Server Contract
The server does not decompress crash blobs during ingest.
It stores the uploaded bytes exactly as received and records part-level content_encoding in the crash index so later tooling can decide whether and how to decode the blob.
Schema Validation
Detritus supports optional per-project JSON Schema validation for crash metadata and OTLP log attributes.
Schema Kinds
The implemented schema kinds are:
crash_metadatalog_attributes
Each schema registration is keyed by (project, kind).
Token File Extension
Schema registrations live in the same TOML file as bearer tokens:
[[schema]]
project = "acme"
kind = "crash_metadata"
path = "schemas/crash.schema.json"
[[schema]]
project = "acme"
kind = "log_attributes"
path = "schemas/log.schema.json"
Schema paths are resolved relative to the token file’s parent directory, not relative to the current working directory.
Validation Behavior
At startup, the server loads and compiles every declared schema into a process-wide registry.
Request handling then applies the registry like this:
- crash uploads validate the
metadataJSON against the project’scrash_metadataschema - OTLP log exports validate each
ResourceLogspayload against the project’slog_attributesschema
If no schema is registered for a given (project, kind) pair, the request is accepted.
Shared references and format policy
Use local resource documents to share definitions across tenant schemas:
[schema_validation]
validate_formats = true
ignore_unknown_formats = false
[[schema_validation.resources]]
uri = "urn:detritus:common"
path = "schemas/common.json"
A registered schema can reference a definition with
{"$ref": "urn:detritus:common#/$defs/build"}. Resource paths are resolved
relative to the token file, just like tenant schema paths. All resources are
prepared at startup; no network or implicit filesystem retrieval is enabled.
Resource URIs must be unique.
If validate_formats is omitted, format validation follows the schema draft’s
default. Setting it to true enforces formats such as email, uuid, and
date-time, including Draft 2020-12 schemas where formats are normally
annotations. ignore_unknown_formats = false makes unknown format names a
startup error when format validation is enabled. Both options preserve the
library defaults when omitted.
Embedded callers can use SchemaRegistry::load_with_options, SchemaOptions,
and SchemaResourceEntry for the same behavior. The implementation uses
jsonschema’s prepared reference registry
and validation options.
Failure Modes
Validation failures are endpoint-specific:
- crash validation failures are rejected on the HTTP path
- log validation failures are rejected on the OTLP/gRPC path
The server also increments validation-failure metrics so operators can distinguish malformed client payloads from transport or auth failures.
Diagnostics include the instance JSON Pointer and the schema keyword path.
The validator’s masked error display hides rejected payload values, allowing
clients to locate a validation failure without echoing their data in the error.
Deployment Note
The NixOS module exposes schemaDir. When set, it is mounted read-only into the service and is intended to hold the JSON Schema files referenced by tokensConfig.
Operations Overview
This page documents the server-side operational contract for detritusd.
Token Configuration
Start the server with:
detritusd --tokens-config /etc/detritus/tokens.toml
The token file uses Argon2 PHC hashes. The literal bearer token is distributed out-of-band and is not stored in the config.
Generate a new hash by supplying one line on stdin:
detritusd hash-token < /run/secrets/receiver-token
The command prints a randomly salted Argon2id PHC string and needs no server configuration. It strips the line ending while preserving spaces in the token, rejects empty input, and does not print the secret. Hashes created with older Argon2 versions continue to authenticate.
[[token]]
id = "regicide-prod"
secret = "$argon2id$v=19$m=19456,t=2,p=1$..."
project = "regicide"
source_prefix = "regicide/"
[[token]]
id = "rs-modde-prod"
secret = "$argon2id$v=19$m=19456,t=2,p=1$..."
project = "rs-modde"
source_prefix = "rs-modde/"
[rate_limit]
logs_per_minute = 1000
logs_burst = 200
crashes_per_minute = 30
crashes_burst = 5
Every request except /healthz and /metrics requires Authorization: Bearer <token>.
Request correlation and shutdown
Every HTTP and gRPC response includes x-request-id. The server preserves a
supplied ID or generates a UUID, and attaches it to the request’s tracing span.
Authorization headers are marked sensitive before request tracing.
Ctrl-C and Unix SIGTERM both initiate graceful shutdown, stop the retention
worker, and drain log writers before the process exits. detritusd --version
prints the installed crate version.
Request Limits
Default limits are per (token, source-id):
logs: 1000 batches/minute, burst 200
crashes: 30 dumps/minute, burst 5
The server returns gRPC ResourceExhausted for logs and HTTP 429 for crashes when a bucket is empty.
Retention
Defaults:
logs TTL: 14 days
crashes TTL: 90 days
janitor: 1 hour interval
Relevant CLI flags:
detritusd \
--logs-ttl-days 14 \
--crashes-ttl-days 90 \
--janitor-interval-secs 3600
The janitor removes expired log files, expired crash source indexes, and then any content-addressed blobs no longer referenced by an index.
Health And Metrics
/healthz is unauthenticated and returns 204 No Content.
/metrics is also unauthenticated and is intended for loopback scraping. It emits OpenMetrics-style text with counters and histograms for request status, ingest volume, dedup hits, writer queue depth, janitor activity, and validation failures.
Ingress Topologies
For the self-hosted reverse-proxy path, see Caddy Reverse Proxy.
For Cloudflare’s hosted edge, body-size limits, and TLS tradeoffs, see Cloudflare Ingress.
NixOS Module
The flake exports a NixOS module for running detritusd in either a single-instance shorthand mode or an explicit multi-instance mode.
Flake Outputs
The repository exports:
packages.<system>.detritus
packages.<system>.docs
packages.<system>.site
nixosModules.default
nixosConfigurations.detritus-test-vm
nixosConfigurations.detritus-multi-test-vm
Single-Instance Shorthand
The existing shorthand interface is:
services.detritus = {
enable = true;
bind = "127.0.0.1:4317";
tokensConfig = config.age.secrets.detritus-tokens.path;
logsTtlDays = 14;
crashesTtlDays = 90;
};
This produces a single detritus.service unit and keeps the historic detritus:detritus runtime identity.
Multi-Instance Mode
Explicit instances use services.detritus.instances:
services.detritus.instances = {
acme = {
bind = "0.0.0.0:4317";
dataDir = "/var/lib/detritus-acme";
tokensConfig = "/run/secrets/detritus-acme.toml";
user = "detritus-acme";
group = "detritus-acme";
openFirewall = true;
};
beta = {
bind = "0.0.0.0:4318";
dataDir = "/var/lib/detritus-beta";
tokensConfig = "/run/secrets/detritus-beta.toml";
};
};
Each explicit instance produces its own:
detritus-<name>.service- system user and group
- token preflight script
- firewall opening when
openFirewall = true
Important Guards
The module enforces several safety checks at evaluation time:
- you cannot mix the top-level shorthand with
services.detritus.instances dataDirmust be listed inknownDataDirs- explicit instances may not share the same
dataDir - explicit instances may not bind the same TCP port
These guards exist to prevent silent data orphaning and port collisions during upgrades.
Schema Files
Each instance optionally accepts schemaDir.
When set, the module:
- grants the service read-only access to that path
- expects schema file references in
tokensConfigto resolve inside that directory tree
Runtime Contract
The systemd unit refuses to start if tokensConfig is not mode 0400 and owned by the configured service user and group.
By default the service stores persistent data in /var/lib/detritus for the shorthand mode and /var/lib/detritus-<name> for explicit instances.
Caddy Reverse Proxy
This page documents the self-hosted ingress path for Detritus on canix hosts.
Topology
Caddy terminates public TLS and forwards cleartext loopback traffic to detritusd.
client
|
| HTTPS
v
Caddy :443
|
| plain HTTP / h2c on loopback
v
detritusd 127.0.0.1:4317
The same upstream listener carries both OTLP/gRPC logs and multipart crash uploads.
Receiver Bind
The correct bind address behind Caddy is:
detritusd --bind 127.0.0.1:4317 --tokens-config /etc/detritus/tokens.toml
Do not bind 0.0.0.0:4317 just to make the proxy work.
canix Route Shape
The canix Caddy preset uses canix.presets.services.caddy.mkReverseProxyRoute. A Detritus host configuration looks like this:
{
config,
inputs,
lib,
...
}: let
hostname = "detritus.example.com";
bindAddr = "127.0.0.1";
bindPort = 4317;
in {
imports = [
inputs.detritus.nixosModules.default
];
services.detritus = {
enable = true;
bind = "${bindAddr}:${toString bindPort}";
tokensConfig = config.age.secrets.detritus-tokens.path;
};
canix.presets.services.caddy.routes = [
(lib.recursiveUpdate
(config.canix.presets.services.caddy.mkReverseProxyRoute {
inherit hostname;
port = bindPort;
host = bindAddr;
})
{
handle = [
{
handler = "reverse_proxy";
transport = {
protocol = "http";
versions = ["h2c" "2"];
};
upstreams = [
{ dial = "${bindAddr}:${toString bindPort}"; }
];
}
];
})
];
}
The important part is the h2c upstream transport, which lets tonic keep serving gRPC over cleartext loopback.
Body Size And Timeouts
The server accepts up to 100 * 1024 * 1024 bytes for each dump or attachment part and applies a request-wide 150 MiB body limit.
If you add a Caddy-side request-body limit, keep it aligned with the server request limit:
request_body {
max_size 150MiB
}
Client Configuration
Clients should point at the public HTTPS origin, not the loopback listener.
detritus-client calls detritus::install_default_crypto_provider() before it builds its HTTPS crash shipper. Applications that construct their own reqwest clients earlier in process startup need to call that function themselves before the first TLS handshake.
Health Check
The receiver exposes GET /healthz, not /health.
Through Caddy:
curl -i https://detritus.example.com/healthz
Cloudflare Ingress
This page explains when Cloudflare’s hosted edge is a practical ingress layer for Detritus and when you should keep TLS termination on your own infrastructure.
For the self-hosted alternative, see Caddy Reverse Proxy.
Recommendation
Cloudflare’s free proxy is practical when all of these are true:
- crash uploads stay comfortably below the
100 MBfree-tier body limit after zstd compression - unary OTLP/gRPC is sufficient
- TLS termination at Cloudflare’s edge is acceptable for your data
Prefer direct ingress, Tailscale, WireGuard, or another self-hosted reverse proxy when any of these are false.
Relevant Cloudflare Limits
Body Size
Free and Pro plans cap request bodies at 100 MB.
For Detritus, that means an oversized crash upload is rejected with HTTP 413 before it reaches detritusd.
gRPC
Detritus uses unary OTLP/gRPC log export, which Cloudflare’s HTTP/2 transit supports.
If your deployment later needs bidirectional or server-streaming gRPC behavior, re-evaluate the proxy decision against Cloudflare’s current product behavior.
TLS
Cloudflare terminates TLS at the edge. The origin leg should run in Full or Full (strict) mode with a valid origin certificate.
If decrypting crash or log payloads at Cloudflare is unacceptable, do not use Cloudflare for Detritus ingress.
Request Timeouts
Long uploads over slow links can time out at the Cloudflare edge. Payload-level zstd compression helps by shrinking crash bodies before upload.
Alternative Topologies
- direct public ingress with your own reverse proxy and certificate
- a Tailscale or WireGuard tailnet
- Cloudflare Tunnel, if the network path matters more than the edge data-visibility tradeoff
Re-Evaluate When
Revisit the decision if:
- real-world uploads hit HTTP
413 - typical crash sizes grow toward the plan limit
- your compliance requirements change
- your ingest surface expands beyond unary gRPC and HTTP multipart uploads
Offline Shipping
Detritus panic hooks write crash reports to a local spool before any network work happens. A later process can flush those pending reports without reconstructing the original SDK configuration by using the stored-config shipper.
Use this path for next-launch cleanup, recovery tools, or a small helper process that only knows the spool directory and has access to a bearer token:
#![allow(unused)]
fn main() {
use detritus::ship_pending_crashes_using_stored_config;
use secrecy::SecretString;
async fn run() -> Result<(), detritus::ShipError> {
let shipped = ship_pending_crashes_using_stored_config(
"/var/lib/my-app/detritus",
SecretString::from("receiver-token"),
)
.await?;
let _ = shipped;
Ok(())
}
}
Each pending entry carries an sdk-config.json written by the panic hook. The
stored-config shipper reads that file for the upload endpoint and sent-entry
retention, so entries in the same spool can be posted to different receivers.
The bearer token is still supplied by the caller and is never stored in the
spool. Keeping the token outside the crash directory lets applications persist
recoverable routing information without leaving long-lived credentials on disk.
Entries missing sdk-config.json, or entries whose stored endpoint is not a
valid URL, fail with ShipError::MissingStoredConfig. That is intentional:
mixed spools produced by older clients should be handled explicitly rather than
silently skipped.
Reusable clients and transport policy
For repeated scans, keep a CrashShipper to reuse HTTP connections:
#![allow(unused)]
fn main() {
use detritus::{CrashShipper, ShipConfig};
use secrecy::SecretString;
async fn run() -> Result<(), detritus::ShipError> {
let shipper = CrashShipper::new(SecretString::from("receiver-token"))?
.with_config(ShipConfig::default().with_dump_compression_level(3));
let shipped = shipper.ship_using_stored_config("/var/lib/my-app/detritus").await?;
let _ = shipped;
Ok(())
}
}
The default connect timeout is 10 seconds, the read timeout is 30 seconds, and
the total request timeout is 60 seconds. Failed or timed-out uploads remain in
pending/. Default HTTP-level retries are disabled: the spool controls replay,
and a single upload attempt should not create multiple crash indexes.
For a private CA, mutual TLS, a proxy, or different timeouts, configure a
Reqwest client
and pass it to CrashShipper::with_client(client, token). Its retry and timeout
settings replace the defaults. Call detritus::install_default_crypto_provider
before constructing a custom HTTPS client.
The convenience shipping functions use the same default transport and share one connection pool throughout each scan. Compression and the on-disk spool format are identical for both APIs.
Releasing
This page consolidates the stable release and publishing procedure for the workspace.
Published Crates
The workspace publishes three crates:
detritus-protocoldetritus-serverdetritus-client
All three stay version-synchronized during 0.x.
Release Order
The release workflow publishes in topological dependency order:
detritus-protocoldetritus-serverdetritus-client
After publishing detritus-protocol and detritus-server, the release workflow waits for the new version to become visible on the crates.io sparse index before continuing.
CI Contract
On every push to trunk and every pull request, CI runs:
cargo fmt --all --checkcargo clippy --workspace --all-features --all-targets -- -D warningscargo check --workspace --all-featurescargo test --workspace --all-featurescargo doc --workspace --no-deps --all-features- an MSRV check on Rust
1.88 cargo audit --deny warningscargo deny --all-features check
CI dry-runs only detritus-protocol:
cargo publish --dry-run -p detritus-protocol --allow-dirty
The dependent crates cannot be dry-run published on arbitrary push and PR commits because their versioned sibling dependencies are resolved against the live crates.io index.
docs.rs Contract
Each published crate carries:
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
That keeps docs.rs builds aligned with the feature-complete public API.
Manual Release Procedure
- Bump every published crate version together.
- Bump the
version = "X.Y.Z"qualifiers on internal path dependencies. - Move the
CHANGELOG.mdunreleased entries under a dated release heading. - Commit the release bump.
- Push the commit to
trunkand wait for CI to go green. - Create and push the
vX.Y.Ztag. - Wait for the release workflow to publish the crates in order.
- Verify the published versions on crates.io and then verify the docs.rs builds.
First Release Checklist
Before the first publish of a crate name:
- verify the crate name is free on crates.io
- publish with the tag-driven workflow
- add any intended co-owners after the first successful publish
Supply-Chain Checks
The repository keeps deny.toml at the root and enforces both cargo-audit and cargo-deny in CI. Treat advisory or licensing failures as release blockers.