SELECT queries over a curated, organization-scoped schema. Every table below is pre-filtered to your workspace before your query runs, so a query can never read another organization’s data — even if you join across tables.
Query rules
Also enforced:
- No semicolons anywhere in the query.
- No SQL comments —
--,/*, and*/are all rejected. - No double quotes, so no quoted identifiers. Use bare
snake_casenames. - Your query must begin with
SELECT. A leadingWITHis rejected — see Common gotchas. - Every table after
FROMorJOINmust be one of the eleven tables listed below.
Identical queries completing within 10 minutes of each other are served from cache and return
cacheHit: true without re-executing.Reading the type column
TIMESTAMP values serialize as ISO 8601 strings. JSONB columns come back as parsed objects. Note that BIGINT and NUMERIC values are returned as strings, not numbers — all the aggregate counts and rate columns in campaign_performance and the email_* counters in contact_360 are affected. Cast in your client before doing arithmetic.
Tables
app_events
Custom events you send from your own product. This is how product behavior — signups, deposits, upgrades — becomes queryable alongside on-chain and messaging data.
Events sent with
test: true are excluded from this table by design, so your test traffic never contaminates a segment or report.occurred_at rather than created_at for time windows — occurred_at is when the event happened in your system, created_at is when we received it.
contacts
Your audience contacts — wallet-first identity with optional email and name.
delivery_events
Per-recipient message events across email and in-app push.
audience_profiles
Wallet profiles with channel reachability and ENS. Keyed by (organization_id, wallet_address) — there is no id column.
audience_segments
Saved segments — wallet lists or rule criteria.
campaigns
Campaigns with status, type, and UTM parameters lifted out of the tracking JSON.
campaign_performance
Per-campaign engagement rollup, ready for reporting. Derived — one row per campaign, no per-recipient grain.
sent counts both email.sent and email.queued. Rates are computed as unique events over sent, rounded to 4 decimal places, and are NULL when nothing has been sent.
user_onchain_metrics
Enriched per-wallet metrics: portfolio value, 90-day activity, LTV estimate, engagement score. One row per wallet.
wallet_activity_summary
Activity summary per wallet per chain.
nft_holdings
NFT holdings per wallet: collections, items, floor prices. One row per (wallet, chain, contract, token_id).
balance is TEXT because ERC-1155 balances can exceed native integer range. Cast it (balance::numeric) before comparing or summing.contact_360
The customer-360 view — one row per contact, joining identity, on-chain metrics, activity, and email engagement, plus derived churn signals. This is usually the fastest place to start.
contact_360 has no organization_id column — it’s already scoped to your workspace.
Column values
SeveralTEXT columns carry a fixed or conventional set of values.
churn_risk
Exactly one of these, never null:
channel
Uppercase: EMAIL, INAPP, TELEGRAM, X, FARCASTER, DISCORD.
event_type
Open-ended, lowercase and dot-separated. The common values are email.queued, email.sent, email.delivered, email.open, email.click, email.bounce, email.complaint, email.unsubscribe, email.failed, and email.suppressed. Because new provider statuses pass through as email.<status>, prefer IN (...) over NOT IN (...) when filtering.
status and type (campaigns)
status: DRAFT, SCHEDULED, SENDING, SENT, ARCHIVED, PAUSED.
type: EMAIL_BLAST, DRIP, SMART_SENDING.
chain and primary_chain
Chain slugs such as eth-mainnet, base-mainnet, matic-mainnet, arbitrum-mainnet, optimism-mainnet, bsc-mainnet, sei-mainnet, and solana-mainnet, plus testnets like base-sepolia-testnet and eth-sepolia. Defaults to eth-mainnet when unspecified.
Joining tables
Joins are fully supported, includingUNION, subqueries, and window functions. Because every table is org-scoped before your query runs, cross-tenant joins are structurally impossible.
wallet_activity_summary and nft_holdings are per-chain and per-token respectively, so joining them to a per-wallet table fans out rows. Aggregate or filter on chain first.
Common gotchas
Query must not start with WITH
Query must not start with WITH
The validator requires the query to begin with
SELECT, so a top-level CTE is rejected. Rewrite it as a subquery:Only read-only queries are allowed — on a SELECT
Only read-only queries are allowed — on a SELECT
The read-only check scans for banned keywords as plain substrings, so ordinary text can trip it. Restructure the predicate or match on a different pattern.
DO , SET , CALL , and COPY are the easy ones to hit:Unsupported table reference(s)
Unsupported table reference(s)
Every name after
FROM or JOIN is checked against the ten tables above, including names you define yourself. Alias your subqueries rather than referencing them as tables.Counts come back as strings
Counts come back as strings
BIGINT and NUMERIC columns serialize as strings. open_rate of 0.2500 arrives as "0.2500". Cast client-side before comparing or charting.
