← All docs

Customer data feeds (multi-tenant)

Problem: you're not exporting for one customer — you're offering "data feeds" as a product feature to every enterprise customer, each with their own bucket, their own schedule, their own format preference.

When to use DataEgress for this

  • Multiple tenants, each needing independent destinations and schedules against the same dataset definition.
  • You want your own dashboard (or your customers') to show per-tenant run history, not just "did the export cron job run today."
  • You want to add this as a sellable feature without your team owning a queue, a cron scheduler, and a multipart-S3-upload implementation.

When NOT to use DataEgress for this

  • You have exactly one customer who wants exactly one export, once. That's a script, not a feed — see export-millions-of-rows.md if it's still large, or just query and download it directly if it's small.

Architecture

One defineDataset call per logical dataset (e.g. "transactions", "events"), reused across every tenant. Tenant isolation happens inside your fetchPage — DataEgress passes tenantId through on every call and never mixes data across tenants because it never aggregates across them itself.

dataset "events"  (one fetchPage, shared)
 ├─ tenant "acme"   -> destination "acme-s3"      -> schedule "0 2 * * *"
 ├─ tenant "globex"  -> destination "globex-gcs?"  -> not supported yet (S3/R2 only, see limitations.md)
 └─ tenant "initech" -> destination "initech-url"  -> destination type "signed_url", no bucket needed

Full implementation

// One dataset definition, reused by every tenant
const events = defineDataset({
  id: "events",
  fetchPage: async ({ tenantId, cursor, limit }) => {
    // tenantId scoping happens here — the only place it needs to happen
    return db.event.findManyPaginated({ tenantId, cursor, limit });
  },
});
export const POST = events.handler();
// Per-tenant setup, e.g. triggered from your own "Enable data feed" UI
async function enableDataFeed(tenantId: string, bucketConfig: S3Config) {
  await dataEgress.createDestination({
    tenantId,
    id: `${tenantId}-s3`,
    type: "s3",
    s3: bucketConfig,
  });
  await dataEgress.createSchedule({
    tenantId,
    dataset: "events",
    destination: `${tenantId}-s3`,
    format: "parquet",
    cron: "0 3 * * *",
  });
}

To show a tenant their own run history, query GET /api/exports and filter by tenantId client-side (there's no server-side ?tenantId= filter yet — see limitations.md).

Common errors

  • Reusing a destination id across tenants — destination ids must be unique; namespace them by tenant (${tenantId}-s3) as shown above.
  • Forgetting a tenant has no destination yetrunExport/createSchedule return destination_not_found with the missing id named explicitly.

Working example

See examples/multi-tenant.