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.
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
| Path | Best for | Delivery contract |
|---|---|---|
| Native gRPC stream | Always-on trusted services and low-latency projection | Replay after a cursor, then follow; at least once |
| Signed webhook | Simple HTTPS integrations, automation and third-party endpoints | Durable history, bounded retry and operator replay |
| HTTP polling | Bootstrap, scripts, migrations and restricted networks | Up to 500 ordered events after a sequence |
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/ExchangeCredentialUse 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/SubscribeThe 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- Begin one application-database transaction.
- Upsert the profile by RustyAuth user UUID; never use email as the primary key.
- Record the event UUID or sequence so retries are harmless.
- Commit the profile update and consumer cursor together.
- 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.unverifiedEach 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
- Keep RPC and webhook secrets in a server-side secret manager.
- Grant only the scopes the consumer needs; profile sync normally needs
events.readandidentity.read. - Persist cursor progress in the same transaction as downstream state.
- Make every handler idempotent and expect at-least-once delivery.
- Constrain webhook destinations with a production egress allowlist or proxy; URL validation cannot close DNS rebinding by itself.
- Alert on stream
DATA_LOSS, webhook terminal failures and a growing delivery backlog. - Exercise credential rotation, webhook signing-secret rotation and replay before launch.
- Pin the evaluated RustyAuth release or commit;
0.1.0remains pre-release software.
Continue with the application integration path, exact API boundary and configuration reference.