Skip to main content

External References

Moonlight uses two columns to anchor an entry to its origin in another system: external_id and external_source. Together they form a unique key per workspace so the same business event can never be ingested twice.

Schema

ColumnTypeDescription
entry.external_idtextStable identifier from the source system, e.g. neary:purchase:100
entry.external_sourcetextName of the source system, e.g. neary
submission.external_idtextOptional submission-level dedupe key

A unique index protects against duplicates:

CREATE UNIQUE INDEX idx_entry_workspace_external
ON entry (project_id, external_source, external_id)
WHERE external_id IS NOT NULL AND external_source IS NOT NULL;

Conventions

External ids should be:

  • Stable: never change after the first send. Replays use the same id.
  • Namespaced: prefix with the source system name and entity type. Example schemes for the Neary integration:
    • neary:purchase:{id} — paid Neary purchase
    • neary:tx:{id} — refund / reversal Transaction
    • neary:affiliate-event:{id} — affiliate bonus / withdrawal
    • neary:user:{id} — customer party
    • neary:provider:{id} — vendor party
  • URL-safe: stick to [A-Za-z0-9:_-]. The id is also used in the voidEntry/getEntry URL paths.

Lookups

The ingest API looks up entries by (workspace_id, external_source, external_id) before inserting. On collision the server records an INGEST_DEDUPED event on the existing entry and returns its id — no duplicate is created.

To query by external id from the SDK:

const entry = await client.getEntry('neary', 'neary:purchase:100');

To list a slice:

const page = await client.listEntries({
externalSource: 'neary',
from: '2026-01-01',
to: '2026-01-31',
limit: 100,
});

Filtering in the UI

The entry list view (moonlight-ui/src/components/EntryList.tsx) has a Source filter input. Type neary (or any other source name) to scope the table.

Refund Linking

When the source system sends relates_to_external_id plus relation_type in the ingest payload, Moonlight resolves the parent entry by external id and writes entry.related_entry_id + entry.relation_type on the new row. This keeps refund entries linked to their originals without requiring callers to know Moonlight's internal ids.

Party Mapping

Parties (employees, customers, vendors) follow the same pattern via the party_external_mapping table. The ingest worker resolves a from_party/to_party object by (integration_id, external_system, external_id) and creates the party only when the mapping does not exist yet — eliminating the legacy Unknown (Auto-created) records.