RudderStack is commonly an event router rather than the only durable owner of event history. The safest migration usually has two parts:
- route new selected events to GraphJSON
- backfill required history from the warehouse, lake, or source system where RudderStack delivered it
Do not assume the RudderStack control plane contains a complete, replayable history for every source and destination.
Inventory the existing pipeline#
For every source, record:
source name and ID:
event types:
SDK or cloud source:
current destinations:
warehouse or archive destination:
transformations:
tracking plan:
consent behavior:
delivery monitoring:
owner:
Identify which destination currently holds the historical event-level data. That system is normally the backfill source.
Choose the live routing pattern#
Recommended:
RudderStack source
→ destination-specific filter and transformation
→ HTTPS adapter you operate
→ GraphJSON bulk logging API
RudderStack documents Webhook sources and destinations and Transformations for custom routing and payload changes.
Use an adapter because:
- GraphJSON expects its API key inside a specific request body
- the key should not become an event property
- the adapter can validate, batch, retry, and quarantine
- the adapter can verify that calls originate from the intended pipeline
Do not publish the GraphJSON key in a transformation or client-side source.
Map the event specification#
Typical RudderStack events use concepts such as:
| RudderStack | GraphJSON |
|---|---|
messageId |
stable event_id |
event on a track call |
bounded event |
originalTimestamp or source occurrence time |
request timestamp |
userId |
user_id |
anonymousId |
anonymous_id |
properties |
allowlisted event fields |
context |
selected platform, source, and campaign context |
type |
source call type |
Use the current RudderStack event specification for the exact source contract.
Do not copy the full context, traits, integrations configuration, or
provider payload into GraphJSON.
Handle call types deliberately#
Track#
Map approved business actions to stable past-tense event names:
Order Completed → order_completed
Report Exported → report_exported
Identify#
An identify call describes traits, not necessarily a business event. Use it only when it represents a meaningful identity transition, such as:
anonymous_identity_linked
user_profile_state_published
Do not create an event for every trait refresh.
Group#
Map account or workspace membership changes when they are real transitions. Do not attach current group traits retroactively to all old events.
Page and screen#
Keep approved, normalized paths or screen names. Remove arbitrary query parameters and high-cardinality routes.
Normalize in the adapter#
function normalizeRudderTrack(input) {
if (input.type !== "track" || !input.event || !input.messageId) {
throw new Error("Unsupported RudderStack event");
}
const occurredAt = new Date(
input.originalTimestamp || input.timestamp
);
if (Number.isNaN(occurredAt.getTime())) {
throw new Error("Invalid occurrence time");
}
const allowed = {
event: toStableEventName(input.event),
event_id: `rudder:${input.messageId}`,
source: "rudderstack",
user_id: input.userId || null,
anonymous_id: input.anonymousId || null,
account_id: input.properties?.account_id || null,
plan: input.properties?.plan || null,
source_name: input.context?.source || null,
schema_version: 1
};
return {
timestamp: Math.floor(occurredAt.getTime() / 1000),
body: allowed
};
}
Replace toStableEventName with an explicit mapping table. Automatically
lowercasing arbitrary names does not preserve meaning.
Filter by destination#
Only route:
- approved event names
- approved sources
- production environment
- consent-eligible traffic
- properties on the GraphJSON allowlist
The transformation should remove forbidden fields before the webhook. The adapter should enforce the allowlist again.
Version the transformation and adapter mapping together.
Backfill from the durable destination#
If RudderStack delivered to a warehouse or lake:
- query the source event table for the required interval
- preserve
messageIdand source occurrence time - apply the same live mapping version
- stage normalized NDJSON
- validate and import in batches
- reconcile by source, event, day, and identity
If no durable destination exists, ask RudderStack what export or replay options apply to the specific deployment. Do not promise historical continuity until the recoverable source is confirmed.
Preserve source transformations#
The warehouse may contain events after a sequence of:
- source SDK normalization
- consent filtering
- tracking-plan enforcement
- user transformation
- destination transformation
Choose whether GraphJSON receives:
- the canonical post-source event
- the existing warehouse representation
- a destination-specific normalized event
Document the exact boundary. Reimplementing only one old transformation can change event names, identity, redaction, or types.
Manage dual delivery#
Use a cutover matrix:
| Period | Existing destination | GraphJSON |
|---|---|---|
| historical | backfill source | import |
| validation | live | live shadow |
| cutover | live or retained fallback | live |
| after cutover | intentional decision | live |
During shadow delivery compare:
- track calls selected
- adapter accepted and quarantined
- GraphJSON accepted
- occurrence-time delay
- counts by event and source
- user and account coverage
- required property completeness
Avoid source-side breakage#
Adding GraphJSON must not:
- block the RudderStack source
- alter payloads sent to existing destinations unexpectedly
- expose the GraphJSON key to the client
- change consent behavior
- turn destination failures into application failures
Use destination-specific transformation and independent failure handling.
Migration checklist#
- Every source, transformation, destination, and owner is inventoried.
- The durable historical source is identified.
- Live events reach a secure adapter rather than exposing the GraphJSON key.
- Track, identify, group, page, and screen calls have explicit policies.
- Event names use a mapping table.
-
messageIdand occurrence time are preserved. - Context, traits, and provider payloads are allowlisted.
- Consent and environment filtering happen before delivery.
- Live and historical paths use the same mapping version.
- Shadow delivery reconciles source, adapter, and GraphJSON counts.
- Existing destinations remain unchanged unless explicitly migrated.
- Cutover and rollback boundaries are recorded.