Developer guide · Events and integration

Connect authentication events to the rest of your product

A new signup should be able to create an application profile, start onboarding or update analytics without coupling your primary database to an authentication ceremony. RustyAuth gives trusted services three deliberate ways to consume the same ordered event log.

RustyAuth records durable, ordered authentication events inside each isolated realm. Your application can follow them as a resumable gRPC stream, poll them during a migration, or let RustyAuth deliver signed HTTPS webhooks. The event is a notification—not a second copy of the identity record.

The integration rule

Use the event’s stable subject UUID as the application identity key. When you need current profile data, fetch the safe projection from the private IdentityService and commit your database update before advancing the event cursor.

Choose the delivery path

PathBest forDelivery contract
Native gRPC streamAlways-on trusted services and low-latency projectionReplay after a cursor, then follow; at least once
Signed webhookSimple HTTPS integrations, automation and third-party endpointsDurable history, bounded retry and operator replay
HTTP pollingBootstrap, scripts, migrations and restricted networksUp to 500 ordered events after a sequence
Isolated realmRustyAuth event logGap-free sequence · redacted data
ContinuousgRPC streamResume from committed cursor
PushSigned webhookVerify, deduplicate, return 2xx
PullHTTP pollBootstrap and migration

What events contain

Every event has a gap-free sequence, UUID, tenant ID, event type, optional subject UUID, timestamp and a redacted JSON object. Useful lifecycle types include identity.created,profile.updated, identifier.added, identifier.removed,identifier.primary_changed, identifier.verified,identifier.unverified and session or credential lifecycle events.

Events deliberately exclude email and phone values, passkey material, cookies, JWTs, assertions and one-time codes. This keeps an event broker or webhook log from becoming a shadow identity database. A trusted backend that needs the current profile calls IdentityService/GetUser with the event subject.

Create a least-privilege consumer

Create a service account in the RustyAuth dashboard with events.read and, only if the consumer projects profiles, identity.read. Create a credential and store the returned rsa_…secret once. Exchange it for a short-lived ES256 bearer token; do not place either credential in browser code.

export RUSTYAUTH_ADDR=localhost:8081

grpcurl -plaintext -import-path ./proto -proto rustyauth/service_accounts/v1/service_accounts.proto -d '{"credential":"rsa_REPLACE_ME","requestedScopes":["events.read","identity.read"]}' "$RUSTYAUTH_ADDR" rustyauth.service_accounts.v1.ServiceAccountService/ExchangeCredential

Use TLS and omit -plaintext outside the local stack. Refresh the access token before its returned expiry; disabling or revoking the underlying credential prevents future exchanges.

Stream signups over gRPC

Start with the last sequence your consumer committed. Zero begins at the oldest retained event. An empty event-type list follows everything; exact filters reduce consumer work without changing the global cursor.

export RUSTYAUTH_ACCESS_TOKEN='eyJ…'

grpcurl -plaintext -H "authorization: Bearer $RUSTYAUTH_ACCESS_TOKEN" -import-path ./proto -proto rustyauth/events/v1/events.proto -d '{
    "afterSequence": "0",
    "eventTypes": [
      "identity.created",
      "profile.updated",
      "identifier.added",
      "identifier.removed",
      "identifier.primary_changed",
      "identifier.verified",
      "identifier.unverified"
    ],
    "checkpointIntervalSeconds": 15
  }' "$RUSTYAUTH_ADDR" rustyauth.events.v1.AuthEventService/Subscribe

The stream first replays records after the cursor and then waits for new ones. Idle checkpoints report the latest sequence, including events excluded by your filter. An expired cursor returns OUT_OF_RANGE; a malformed or missing sequence returns DATA_LOSS instead of silently skipping work.

Project a signup into your primary database

On identity.created or a later profile/identifier event, call the private identity service with the stable subject UUID. The response includes the safe profile, identifiers and passkey metadata—not stored WebAuthn public keys, counters, sessions or tokens.

grpcurl -plaintext -H "authorization: Bearer $RUSTYAUTH_ACCESS_TOKEN" -import-path ./proto -proto rustyauth/identity/v1/identity.proto -d '{"userId":"THE_EVENT_SUBJECT_UUID"}' "$RUSTYAUTH_ADDR" rustyauth.identity.v1.IdentityService/GetUser
  1. Begin one application-database transaction.
  2. Upsert the profile by RustyAuth user UUID; never use email as the primary key.
  3. Record the event UUID or sequence so retries are harmless.
  4. Commit the profile update and consumer cursor together.
  5. Only then acknowledge progress by using that sequence on the next subscription.

Your application database should continue to own roles, billing state, entitlements and resources. RustyAuth owns authentication identity.

Use durable signed webhooks

Declare stable destinations in rustyauth.yaml when infrastructure as code should be authoritative, or create a dashboard-managed destination through WebhookService. A creation or secret rotation returns thewhsec_… signing secret exactly once. A YAML-managed destination can be tested, inspected, replayed and have its secret rotated, but its URL and filters remain owned by YAML.

spec:
  webhooks:
    - id: application-profile-sync
      name: Application profile sync
      endpoint: https://api.example.com/hooks/rustyauth
      enabled: true
      eventTypes:
        - identity.created
        - profile.updated
        - identifier.added
        - identifier.removed
        - identifier.primary_changed
        - identifier.verified
        - identifier.unverified

Each delivery includes:

  • x-rustyauth-delivery — the idempotency key for this delivery.
  • x-rustyauth-event — the event type.
  • x-rustyauth-timestamp — Unix seconds used in the signature.
  • x-rustyauth-signature: v1=… — lowercase hex HMAC-SHA256.
use std::time::{SystemTime, UNIX_EPOCH};
use hmac::{Hmac, Mac};
use sha2::Sha256;

type HmacSha256 = Hmac<Sha256>;

fn unix_seconds() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system clock before Unix epoch")
        .as_secs()
}

fn verify_rustyauth_webhook(
    secret: &str,
    timestamp: &str,
    signature: &str,
    exact_body: &[u8],
) -> anyhow::Result<()> {
    let sent_at: u64 = timestamp.parse()?;
    anyhow::ensure!(unix_seconds().abs_diff(sent_at) <= 300, "stale webhook");

    let supplied = signature
        .strip_prefix("v1=")
        .ok_or_else(|| anyhow::anyhow!("unsupported signature version"))?;
    let supplied = hex::decode(supplied)?;

    let mut mac = HmacSha256::new_from_slice(secret.as_bytes())?;
    mac.update(timestamp.as_bytes());
    mac.update(b".");
    mac.update(exact_body);
    mac.verify_slice(&supplied)?; // constant-time comparison
    Ok(())
}

Verify the signature against the untouched request bytes before parsing JSON. Reject stale timestamps, then deduplicate on the delivery ID and return a 2xx only after your durable work commits. RustyAuth retries transport failures, 408, 425, 429 and 5xx responses with bounded exponential backoff. Redirects are never followed. Operators can inspect history and replay a failed delivery while its source event remains retained.

Use HTTP polling for bootstrap work

curl -sS -H "x-bootstrap-token: $BOOTSTRAP_TOKEN" "http://localhost:8081/v1/events?after=0"

Polling returns up to 500 subsequent ordered records. The bootstrap token is administrative and is best kept for controlled migrations or local evaluation; use scoped service accounts for long-running gRPC consumers.

Production checklist

  1. Keep RPC and webhook secrets in a server-side secret manager.
  2. Grant only the scopes the consumer needs; profile sync normally needs events.read and identity.read.
  3. Persist cursor progress in the same transaction as downstream state.
  4. Make every handler idempotent and expect at-least-once delivery.
  5. Constrain webhook destinations with a production egress allowlist or proxy; URL validation cannot close DNS rebinding by itself.
  6. Alert on stream DATA_LOSS, webhook terminal failures and a growing delivery backlog.
  7. Exercise credential rotation, webhook signing-secret rotation and replay before launch.
  8. Pin the evaluated RustyAuth release or commit; 0.1.0 remains pre-release software.

Continue with the application integration path, exact API boundary and configuration reference.

Try the complete boundary

Run passkey authentication locally.

Start locally