recalled.dev
Core concepts

What to log

The hardest part of integrating Recalled is not the SDK call, it is deciding what is worth logging. Log too little and your audit trail is useless. Log too much and you blow your quota, slow your app and drown the signal in noise.

This page is the opinionated guide. Follow these rules and you will have a clean, useful, compliant audit log without thinking about it.

The 3 rules

1. Log state changes, not reads. A user reading their dashboard 50 times a day is not an audit event. A user changing their email address once is. If the action does not mutate something in your system or in the user's account, it does not belong in Recalled.

2. Log actions with consequences, not technical noise. A failed health check, a cache miss, a 304 response, a CDN purge: not audit events. They belong in your APM tool. Recalled is for actions a human or an auditor would care about in 6 months.

3. Log what tells a story. Imagine someone in 6 months asking "who deleted this invoice and why". The answer must come from a single event: the actor, the target, the reason if any, the IP, the time. If your log entry does not let you reconstruct the story, it is incomplete.

Decision flowchart

When you are about to add a client.events.create() call somewhere, ask:

  1. Does this change a piece of state in our system? No → do not log.
  2. Would the legal, support or security team ever ask "who did this and when"? No → do not log.
  3. Is the cardinality reasonable? (less than 10 events per active user per session) Yes → log it. No → consider sampling or aggregating client-side.

If you answer yes to 1 and 2 and the cardinality is reasonable, log it. Otherwise skip it.

Catalogue by category

The standard list of what most B2B and B2C SaaS should log. Pick the categories that apply to your product, copy the action names, adapt to your domain. Each tab carries the events to log, the ones to skip, and a metadata hint.

Authentication

Every action that changes who is logged in or how they authenticate.

Log this
  • user.signed_up (new account creation)
  • user.logged_in (successful auth, every method)
  • user.logged_out (explicit logout, including session revocation)
  • user.login_failed (bad password, expired token, blocked IP). Critical for security.
  • user.password_changed
  • user.password_reset_requested and user.password_reset_completed (two distinct events)
  • user.email_changed
  • user.two_factor_enabled and user.two_factor_disabled
  • user.session_revoked (admin force-logout, log out from all devices)
  • magic_link.sent and magic_link.consumed
  • oauth.linked and oauth.unlinked (Google, GitHub, etc.)
Skip this
  • ×Page views or user navigated to /dashboard
  • ×Token refresh (every 5 minutes is noise, log only revocations)
  • ×Auth health checks
  • ×Successful CSRF token verifications
Useful metadata

auth_provider (email, google, magic_link), ip, user_agent, mfa_used (true/false), success (for failed attempts include the reason: bad_password, account_locked, mfa_required).

Authorisation

Anything that changes who can do what.

Log this
  • member.invited, member.joined, member.removed
  • member.role_changed (always include before/after in metadata)
  • team.created, team.deleted
  • permission.granted, permission.revoked
  • api_key.created, api_key.revoked. Last_used_at on the key is fine, do not log every single use
  • sharing.granted, sharing.revoked (when a user shares a resource with another user or externally)
  • ownership.transferred (especially for B2B)
Skip this
  • ×Permission checks (every API request runs them, that is APM territory)
  • ×Read-only access grants if they auto-expire fast
  • ×Internal RBAC computation steps
Useful metadata

role_before, role_after, scope, granted_by, expires_at.

Data lifecycle

The heart of audit logging. Every business object your users create, update, delete, share or move.

Log this
  • <object>.created (invoice.created, project.created, document.created, etc.)
  • <object>.updated (with a changed_fields array in metadata, not the full diff if it is large)
  • <object>.deleted (always, every soft-delete and hard-delete)
  • <object>.archived and <object>.restored
  • <object>.published, <object>.unpublished
  • <object>.duplicated (by whom, source id in targets)
  • <object>.moved (folder change, ownership change, etc.)
Skip this
  • ×Auto-saves and draft updates if your app saves every 10 seconds. Log the explicit save instead.
  • ×Reads, opens, views (those go to product analytics, not audit)
  • ×Internal denormalisation jobs that touch the same row
Useful metadata

changed_fields (array of field names), reason (if user provided one), source (manual, api, import, automation).

Money

Every monetary state change. Be exhaustive here, accountants and your future self will thank you.

Log this
  • subscription.created, subscription.updated, subscription.canceled
  • subscription.plan_changed (with from and to plan slugs in metadata)
  • invoice.created, invoice.paid, invoice.failed, invoice.refunded
  • payment.succeeded, payment.failed (include declined reason)
  • refund.issued, refund.completed
  • coupon.applied, coupon.expired
  • payment_method.added, payment_method.removed, payment_method.set_default
Skip this
  • ×Stripe webhook ping itself (log the event the webhook represents instead)
  • ×Currency conversion checks
  • ×Pre-flight payment validations that did not result in a charge attempt
Useful metadata

amount_cents, currency, stripe_payment_intent_id, provider (stripe, paddle, etc.), reason for refunds.

Admin actions

Anything an internal team member does on behalf of a user. Always log these, no exception. This is what auditors look at first.

Log this
  • admin.impersonation_started, admin.impersonation_ended
  • admin.user_unlocked, admin.user_locked
  • admin.feature_toggled (per user or globally)
  • admin.data_overridden (when support manually fixes a value)
  • admin.support_intervention (catch-all for one-off corrections)
  • Anything done via your internal back-office tools that mutates customer data
Useful metadata

admin_id, reason (always require one in your back-office UI), ticket_id if you link to your helpdesk.

Exports & imports

Data leaving or entering the system in bulk.

Log this
  • export.started, export.completed, export.failed
  • import.started, import.completed, import.failed
  • bulk_delete.requested, bulk_delete.completed
  • gdpr.access_request and gdpr.erasure_request
Useful metadata

format (csv, json), row_count, destination for exports, source for imports, size_bytes.

Integrations

Events that involve third parties.

Log this
  • integration.connected, integration.disconnected
  • webhook.created, webhook.updated, webhook.deleted
  • webhook.delivery_failed (after the retry budget is exhausted, not every retry)
  • oauth.token_refreshed only if it represents a meaningful event (e.g. forced re-auth), otherwise skip
Skip this
  • ×Every successful webhook delivery
  • ×Healthchecks against integrations
Security

Things your security team needs visibility on.

Log this
  • security.brute_force_detected (after the threshold is hit)
  • security.suspicious_login (new country, new device, etc.)
  • security.rate_limit_exceeded (only if it persisted, not every hit)
  • security.csp_violation_reported
  • security.api_key_leaked (if you have a detection pipeline)
Skip this
  • ×Every rate-limited request individually
  • ×Every CSP violation (sample or aggregate hourly)
System & jobs

Useful for operational debugging, but be careful with cardinality.

Log this
  • cron.<name>.completed and cron.<name>.failed (one per run, not per item processed)
  • migration.applied and migration.rolled_back
  • backup.created, backup.restored
Skip this
  • ×Each iteration of a loop, each row processed by a batch job
  • ×Heartbeats, liveness probes
  • ×Internal queue moves

Naming conventions

Follow this schema for everything: <domain>.<subject>.<verb_past_tense> or <domain>.<verb_past_tense> when there is no separate subject.

Rules:

  1. All lowercase, dot-separated, snake_case if needed inside a segment.
  2. Past tense verbs: .created not .create, .updated not .update, .deleted not .delete.
  3. Domain first, subject second, verb last: invoice.line_item.added is better than added.invoice.line_item.
  4. Be consistent: pick a verb and stick with it. Do not mix .deleted and .removed for the same domain. Do not mix .failed and .errored.
  5. Keep it stable: action names end up in your dashboards, retention rules and notification filters. Renaming them later breaks history.

Standard verbs to reach for, ranked by frequency:

VerbUse it for
.createdNew resource came into existence
.updatedExisting resource changed
.deletedResource removed (soft or hard)
.archived / .restoredSoft state changes
.published / .unpublishedVisibility toggles
.shared / .unsharedAccess grants and revocations
.signed_up / .logged_in / .logged_outAuth lifecycle
.invited / .joined / .removedMembership
.started / .completed / .failedLong-running async actions
.requested / .granted / .revokedPermission flows

Metadata schema

metadata is a free-form JSON object. Keep it small (a few hundred bytes is plenty), structured, and useful in 6 months. The patterns below work across most action categories.

Always include when relevant

  • source: where the action came from. Values: web, mobile, api, admin_panel, automation, import, webhook.
  • reason: free text reason if the user or admin provided one (metadata.reason: "duplicate").
  • request_id: your own correlation id for tracing back to logs.
  • result: success or failure. Default to success so you can filter failures later.

For updates

json
{
  "metadata": {
    "changed_fields": ["status", "due_date"],
    "before": { "status": "draft" },
    "after": { "status": "paid" }
  }
}

Only include the changed fields, not the full object. If the diff is huge, just keep changed_fields and skip before/after.

For money

Always serialise amounts in integer cents (or smallest currency unit), never floats:

json
{
  "metadata": {
    "amount_cents": 4200,
    "currency": "EUR",
    "stripe_payment_intent_id": "pi_..."
  }
}

For failures

json
{
  "metadata": {
    "result": "failure",
    "reason": "card_declined",
    "code": "insufficient_funds"
  }
}

Antipatterns

Do not put any of these in metadata:

  • Plaintext secrets, passwords, full tokens, full credit card numbers. If it should not be in your logs, it should not be in Recalled.
  • Full document bodies, file contents, blobs, images. Recalled is not a document store. Put the resource id in targets and let the consumer fetch the body if they need it.
  • PII you do not need. If you log an action on a user, you already have actor.id. You do not also need their full address, phone number, etc. Less PII = less GDPR exposure.
  • Server-side debug data. Stack traces, SQL queries, internal IDs that mean nothing to a human auditor. Send those to your APM.
  • Anything bigger than ~2 KB. Large metadata slows down list queries and inflates your storage bill.

Volume guidance

Recalled bills by event count per month. The right ratio for most B2B SaaS:

  • 1 to 5 events per active user per session (login, a couple of actions, logout)
  • 0 events when a user is just reading
  • A handful per day for system actions (cron, billing, backups)

If you find yourself logging more than 10 events per active user per day, you are probably over-logging reads or capturing technical noise. Re-read the 3 rules at the top.

Worked example

A fictional B2B invoicing SaaS. Here is the complete list of events its team would push to Recalled. Notice how it stays focused.

text
# Auth (5)
user.signed_up
user.logged_in
user.logged_out
user.login_failed
user.password_changed

# Team (4)
member.invited
member.joined
member.removed
member.role_changed

# Invoices, the core business object (6)
invoice.created
invoice.updated
invoice.sent
invoice.paid
invoice.refunded
invoice.deleted

# Customers (3)
customer.created
customer.updated
customer.deleted

# Billing for the SaaS itself (4)
subscription.created
subscription.plan_changed
subscription.canceled
payment_method.added

# Admin (2)
admin.impersonation_started
admin.impersonation_ended

# Exports (2)
export.started
export.completed

That is 26 distinct actions, more than enough for a clean audit trail. Your real list will be similar in size.

Next step

Once you have your list, drop it in the Events API and start sending. The dashboard's list_actions_summary tool helps you see in real time which actions you are logging the most, so you can spot over-logging or gaps.