Push gateway service in Rust.
After cloning, enable the project's pre-commit hooks:
git config core.hooksPath .githooksThe hook runs cargo fmt --all -- --check and cargo clippy --no-deps -- -D warnings on staged Rust changes. If .githooks/pre-commit is missing on
a branch, copy it from
arkret-rust-sdk and
adapt to your local toolchain.
floria forwards ak.device.push_route and ak.edge.push.command.notify.v1 events that
target a specific security boundary:
- Realm: security boundary membership, capability, E2EE, federation.
- Space: navigation container board, list, section, calendar bucket; lives inside a Realm.
The inbound push notify model uses realm_id for the security boundary and
hard-rejects space_id. Provider-facing blind wakeups strip Realm, Space,
Strand, Event, Message and actor identifiers before dispatch.
Spec round 4 (arkret-spec range 2a4d39b..a77b995, 8 commits) adds
three push-pipeline behaviours. See CHANGELOG.md
[Unreleased] for the canonical wire-breaking list.
mention_redirect_target_route_tokensplaintext routing message / system-message payloads now carry an explicit redirect-target actor list. If the recipient device's actor is not in the list, floria runs fail-closed: a blind wakeup goes out, the content is not decrypted.historical_onlyis push-quiet when an upstream service attachesreason_code=historical_onlyto a federation idempotency replay, floria does not run a fresh push fanout. The event is diagnostic only.e2ee_late_recoveryaudit pathak.audit.policy_access{access_kind=e2ee_late_recovery}routes through the audit pipeline and never produces a push wakeup.
Spec rounds 2+3 (2026-05-20) extended the blind-wakeup sanitizer's
forbidden-field list (attestation_evidence,
audit_purpose, attestation_chain, audit_policy_version_digest,
trust_domain) and extended the gateway integration. The former identity-bearing
account-deactivation broadcast has been retired. Stations revoke registrations
through the formal signed /_arkret/edge/push/registrations:apply surface using
opaque registration and pairwise target ids; only a verified durable Gateway
receipt completes that leg. Contact/DM, Agent participation,
Sidecar, and operation-control admission stay upstream of the push gateway. See
CHANGELOG.md [Unreleased] and
push-notifications.md
for the normative source. Signal kinds (ak.presence, ak.typing,
ak.receipt.read, ak.call.signal) are confirmed to bypass floria
entirely; they ride the encrypted SignalEnvelope rail in the Sync
Service (spec/v1/zh/sync/signal.md). The former plaintext realtime rail
and transaction-body field are gone, superseded by SignalEnvelope /
signals.
salvofor HTTP APIreqwestfor outbound APNS / FCM callskdlfor KDL config (default)serde-saphyrfor YAML config (also supported)
PostgreSQL stores formal opaque registration handoff state and can back configured registration sources and the retry dead-letter overlay.
POST /_arkret/edge/push/notifyas the canonical Arkret notify endpointGET /_arkret/describegateway profile discovery at the root meta positionGET /healthGET /readyGET /readyz- Prometheus metrics on a dedicated
/metricslistener - app id exact match and glob match
- per-pushkin in-flight concurrency limit
- optional in-memory or Redis dedup cache for successful
/notifyrequests - optional in-memory
/notifyrate limits with429+Retry-After /notifyservice auth via HTTP Message Signature, bearer fallback, and optional mTLS profile- APNS certificate auth and token auth
- FCM HTTP v1
- JPush REST v3 with
third_party_channelpassthrough - Huawei Push Kit / HarmonyOS server push
- HONOR Push Kit server push
- OPPO / OPlus / OnePlus server push
- vivo Push server push
- Xiaomi Mi Push server push
- WebPush / VAPID
FLORIA_CONFenv varHTTPS_PROXYenv var fallback for outbound proxying- KDL config (default) and YAML config, detected by file extension
- structured logging via
log.setup(text or JSON formatter, EnvFilter /RUST_LOG) - distributed tracing via
metrics.opentracing(OTLP / OpenTelemetry, gRPC) - error reporting via
metrics.sentry(DSN, environment, release, sample rates)
The config format is detected by file extension:
.kdlKDL (default whenFLORIA_CONFis not set).yaml/.ymlYAML
See docs/en/configuration.md for the full configuration reference.
See docs/en/server-integration.md for coland / server-side /notify integration.
See docs/en/mobile-integration.md for chime / mobile-client integration boundaries.
See docs/en/credential-rotation.md for the credential rotation runbook.
See docs/en/reverse-proxy.md for the TLS-termination / mTLS reverse-proxy reference (sample nginx and Caddy configs in examples/reverse-proxy/).
See docs/en/supply-chain.md for local Trivy, Syft SBOM, and Cosign/SLSA provenance commands.
See docs/en/openapi.json for the local OpenAPI artifact.
Quick notes:
proxyin the config file takes precedence overHTTPS_PROXY- config file strings are not environment-expanded;
${ENV_VAR}remains literal unless an external deploy step renders it before startup - floria does not hot reload config or secrets; use a process restart or rolling restart for every config change
- 1.0 deployments should use an external secret manager, sidecar, or init step to mount/render APNs, FCM, VAPID, custom push, and service-auth secrets before floria starts
metrics.prometheusstarts a separate listener, defaulting to127.0.0.1:8000- import
docs/en/grafana-dashboard.jsonfor the recommended Prometheus dashboard - unknown config sections / fields emit startup warnings
- the
memorydedup backend is single-instance only; use Redis-backed dedup for HA deployments push_hintis a body-free wakeup hint and must not contain plaintext message content- the push gateway is a derived wakeup surface, not canonical truth for events or unread state
/_arkret/edge/push/notifyaccepts authenticated service calls and supports theIdempotency-Keyheaderak.edge.push.command.notify.v1accepts only the SDKPushNotifyRequestBody; operation identity and the source/destination service DIDs stay in the URL and signed transport headers, never in the body- error responses use a JSON envelope with
capability_denied,unsupported_feature,schema_violation,payload_too_large,rate_limited, ortemporarily_unavailablefor gateway contract failures - E2EE wakeups are validated as blind/minimized payloads: message body, encrypted payload bytes, SDP, ICE, and TURN credentials are rejected
- unauthorized callers cannot attach
sender_actor_display_name,strand_name,space_name,sender,target_did, or nesteddid:literals inside notification/default payload fields - notify requests use the current
push_target_id,wakeup_kind, andpush_keyfield names; unknown notification fields are rejected by the wire model - notify responses conserve
(push_target_id, device_id)outcomes and expose neither raw push tokens nor token hashes - provider delivery receipts and token hashes are gateway-private telemetry and never appear in the synchronous notify response
- bearer fallback is header-only; query string auth material is rejected
- WebPush endpoints must match the configured allowlist and must not include query strings
- lightweight readiness probes hit
GET /ready; strict readiness probes can useGET /readyzto require a populated provider registry and reachable Redis-backed dependencies
Sample files:
floria.sample.kdlKDL config with all providers commented outfloria.sample.yamlYAML config with all providers commented out
- For mainland Android delivery, prefer
JPushas the broad aggregation layer. - Keep direct
Huawei Push Kit,HONOR Push,Xiaomi Mi Push,OPPO / OnePlus Push, andvivo Pushenabled for apps that already hold first-party credentials or need tighter channel control. - Use JPush
third_party_channelto steer Huawei/Xiaomi/OPPO/vivo/HONOR/HMOS routing without adding another backend integration per OEM. - JPush currently has no explicit
third_party_channel.oneplusvendor key; OnePlus should use the dedicatedoneplusprovider.
$env:FLORIA_CONF="$PWD\floria.sample.kdl"
cargo runBuild the image:
docker build -t floria .Generate local supply-chain artifacts without publishing an image or Git tag:
.\scripts\local-supply-chain.ps1 -Image floria:local-supply-chain -SkipCosignRun it with a mounted config file:
docker run --rm -p 5000:5000 -p 8000:8000 -v ${PWD}/floria.sample.kdl:/app/floria.kdl floriaAn example compose.yml is provided in the examples/ directory.
For the smallest config and one canonical notify request shape, see examples/README.md, examples/minimal.kdl, and examples/minimal.notify.request.json.
cp floria.sample.kdl examples/floria.kdl
cd examples
docker compose up -dSee examples/compose.yml for full instructions.
Before exposing floria to the public internet, walk every item below.
The same list will be computed at runtime and surfaced on
/health.hardening so codmin's /hardening dashboard can flag failing
checks across the whole fleet (see T8.3 for the cross-service shape).
-
http.notify_auth.production_mode true(surfaced ashardening.admin_auth_mode) - TLS terminated at the reverse proxy — floria has no in-process TLS listener
- CSP header configured at the reverse proxy
- CORS limited to the allowed origins for station callers
- Secrets sourced from an external secret manager and mounted/rendered before startup (APNs auth key/cert, FCM service account, VAPID keys, custom push secrets, service-auth keys)
- Log redaction enabled (default outside dev mode)
- Admin auth in production mode (no dev bypass)
- Rate limit enabled
- Provider credential rotation scheduled for APNs / FCM / VAPID
Licensed under Apache 2.0. See LICENSE.
Notify devices contain only device_id. Floria resolves the provider token and
visible-notification opt-in from the Station's current durable registration.
For a deployment sharing a trusted PostgreSQL boundary with Coland, configure:
http:
public_base_url: https://push.example/
registration_sources:
"ak:did_core:web:station.example":
postgres_url: "postgresql://push_registration_reader@db/coland"
provider_timing_bucket_seconds: 60The source must also be an authenticated notify_auth.service_principals entry.
Use a database role with SELECT access only to public.push_devices; no gateway
schema creation or registration writes are performed. The registration gateway
URL must resolve to this exact public base URL. Unknown, expired, removed or
mismatched registrations never reach a provider. Retries resolve registrations
again and do not retain provider tokens. Registration opt-in changes apply to
subsequent dispatches.
Registration state has two supported sources: shared authoritative storage
inside the same operational, logging, audit and trust boundary as the Station,
or the signed opaque registration handoff in push-notifications.md §3.4.
An independently stored Gateway uses
POST /_arkret/edge/push/registrations:apply; the Station verifies and durably
saves the Gateway receipt before considering that registration available.
Account and principal identities remain inside the Station boundary. Notify
cannot create or repair an unknown registration.
The postgres_registration_identity_rotation_and_removal regression requires
FLORIA_REGISTRATION_TEST_DATABASE_URL naming an isolated database initialized
with Coland's initial schema. It never creates SQL files or copies that schema.