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
| Column | Type | Description |
|---|---|---|
entry.external_id | text | Stable identifier from the source system, e.g. neary:purchase:100 |
entry.external_source | text | Name of the source system, e.g. neary |
submission.external_id | text | Optional 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 purchaseneary:tx:{id}— refund / reversal Transactionneary:affiliate-event:{id}— affiliate bonus / withdrawalneary:user:{id}— customer partyneary:provider:{id}— vendor party
- URL-safe: stick to
[A-Za-z0-9:_-]. The id is also used in thevoidEntry/getEntryURL 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.