Architecture
The Native App is a hub-and-spoke deployment. The hub is a small Zeotap cloud control plane that handles browser auth and reverse-proxies UI traffic. The spokes are the SPCS services that run inside each customer’s Snowflake account. Each spoke is fully self-contained — it owns its metadata store, its event buffer, and the application-owned warehouse it executes SQL against — and never reaches across to another customer’s install.
SPCS service inventory
Zeotap runs four long-running SPCS services and a varying number of ephemeral job services inside your account.
| Service | Role | Runtime |
|---|---|---|
zeotap_control_plane | Same Zeotap API used by the cloud product. Serves UI requests forwarded by the cloud reverse-proxy, and accepts events on its /v1/* endpoints (track, identify, page, screen, group, batch) for the in-account event hot path. | Long-running |
zeotap_postgres | Metadata store for workspaces, sources, syncs, audiences, identity graphs, orchestrations, and run history. Persists to an SPCS block volume. | Long-running |
zeotap_redis | Streaming buffer for the event hot path. Decouples ingest from the forwarder so SDKs aren’t blocked on destination latency. | Long-running |
zeotap_event_forwarder | Batch-consumes the streaming buffer and runs two lanes per batch: forwarding rules to destinations, and warehouse delivery to per-event-type tables (tracks, identifies, pages, …). | Long-running |
zeotap_job_* | Ephemeral SPCS job services for sync runs, loaders, computed attribute computations, identity resolution, orchestration execution, and AI agent sessions. | One-shot |
Service-to-service connections
The control plane is the only service that talks to all the others. It reads and writes Postgres for metadata, publishes to Redis when it accepts incoming events on its /v1/* endpoints, and dispatches sync, loader, identity, computed attribute, orchestration, and agent work as one-shot SPCS job services. The forwarder consumes from Redis and writes both to destinations (through the bound EAIs) and to your Snowflake databases via ZEOTAP_ADMIN_WH, the warehouse the application provisions at install time.
Job services don’t have a long-running connection to the rest of the graph. When the control plane creates one, it passes the work payload through SPCS service arguments. The job posts run-state updates back to the control plane over HTTP (authenticated by a run-scoped JWT), and opens its own connections to ZEOTAP_ADMIN_WH for SQL execution and to whatever EAIs the work requires. When the job finishes, SPCS removes the service.
The application-owned warehouse
ZEOTAP_ADMIN_WH is created and owned by the application itself. The application requests CREATE WAREHOUSE in its manifest privileges; PROVISION_SERVICES issues CREATE WAREHOUSE IF NOT EXISTS ZEOTAP_ADMIN_WH WAREHOUSE_SIZE = XSMALL AUTO_SUSPEND = 60 AUTO_RESUME = TRUE at install time, and every SPCS service spec sets QUERY_WAREHOUSE = ZEOTAP_ADMIN_WH directly.
You can resize the warehouse from the setup app’s Warehouse settings card or by calling <APP_NAME>.APP_DATA.SET_WAREHOUSE_SIZE('<size>'). Valid sizes range from XSMALL through X6LARGE; the proc validates the value before issuing ALTER WAREHOUSE. Because the warehouse is application-owned, there is no consumer-side GRANT USAGE or GRANT OPERATE step and no customer_warehouse reference to bind.
The proxy chain
Browser-to-SPCS traffic takes four hops, each enforcing a different authentication contract.
- Browser → cloud. The user signs into
composable.zeotap.comwith email or Google. The UI sends an HTTPS request with a Firebase ID token in theAuthorizationheader. - Cloud → SPCS public ingress. The cloud control plane validates the Firebase token, looks up which Native App install the user’s workspace is bound to, fetches the encrypted keypair credential for that install, and re-signs the request with a JWT bearer based on the
${BRAND}_PROXY_USERkeypair. It POSTs the original request body to the install’s SPCS public ingress URL. - SPCS public ingress → control-plane container. Snowflake’s SPCS gateway terminates TLS, validates the JWT against the proxy user’s registered RSA public key, and forwards the request to the
zeotap_control_planeservice over the internal SPCS network. - Control plane → Postgres / warehouse. The control plane processes the request locally — read the metadata, run a sync, query the bound database — and writes the response back through the same chain in reverse.
The cloud never persists Firebase tokens; it mints a JWT per request and discards it. The keypair private key never leaves the cloud’s encrypted credential store. The customer’s data never round-trips through the cloud — only API request bodies and response bodies pass through, and the cloud doesn’t store them.
Event ingestion data flow
Events take a separate path optimised for high throughput and at-least-once delivery.
- SDK to cloud ingest. Web, mobile, and server SDKs POST to
events.zeotap.com/v1/trackregardless of whether the workspace runs cloud or Native App mode. The cloud ingest validates the write key. - Cloud ingest to in-account control plane. When the write key is bound to a Native App install, the cloud forwards the validated payload to that install’s
zeotap_control_planeservice through the same proxy chain used for UI calls — the/v1/*event routes share the same public ingress as the API. - Control plane to Redis stream. The control plane authenticates the forwarded write key against the install’s local key store, enriches the payload with workspace metadata, appends it to a Redis stream, and returns 200 to the cloud, which returns 200 to the SDK. End-to-end latency from the SDK perspective is the proxy round-trip plus a Redis
XADD. - Forwarder fan-out. The forwarder polls the Redis stream and runs two lanes per batch:
- Forwarding rules — real-time delivery to destinations through the bound EAIs. Server-side CAPI batchers, generic webhooks, and per-platform adapters all run in this lane.
- Warehouse delivery — buffered batched INSERTs to
CDP_EVENTS.RAW_EVENTSin the database your destination targets. Same buffer policy (size and time triggers) as cloud-mode warehouse delivery, but the write happens through the in-process Snowflake driver session — no Avro, no GCS staging, noCOPY INTO.
- At-least-once with
messageIddedup. SDKs already de-dup their own retries bymessageId. Custom server-side integrations should set a stablemessageIdper event so the forwarder’s idempotent destination writes can de-dup retries downstream.
Schemas Zeotap creates
In each Snowflake database you configure as a source or destination:
| Schema | Contains |
|---|---|
CDP_EVENTS | RAW_EVENTS table holding all events the forwarder writes. |
CDP_PLANNER | Plan tables for sync orchestration (a/b slot rotation for diff-based CDC). |
CDP_AUDIT | Audit tables for sync runs and pipeline observability. |
Inside the application’s own footprint (<APP_NAME>.APP_DATA):
| Object | Contains |
|---|---|
APP_VERSION | The currently installed application version. |
CLOUD_REGISTRATION | The cloud-issued install ID, claim URL, and registration status. Written by the SPCS control-plane service when the cloud handshake completes. |
CLOUD_CREDENTIALS | The keypair private key minted by GENERATE_PROXY_KEYPAIR, plus the proxy user/role names and the cloud-side register_secret. |
REFERENCE_BINDINGS | The EAI references bound by the application — populated by REGISTER_REFERENCE_CALLBACK when Snowsight reports a Grant event from the Configure tab. |
REGISTRATION_REQUEST | Single-row queue the setup app writes to when you click Register with Zeotap. The SPCS control-plane service watches this table and performs the cloud handshake through its zeotap_sf_api_access EAI binding. |
The application schemas are inside the app sandbox — only the application’s own procedures can read them, and they’re invisible to your other Snowflake roles.
Why the SPCS control plane drives cloud registration
Cloud registration runs as a goroutine inside the zeotap_control_plane SPCS service rather than as a stored procedure. The reason is structural: Snowflake’s marketplace review accepts EAIs provisioned only through the Permissions SDK consent flow, and that flow binds USAGE through manifest references rather than directly on the integration object. Stored procedures with EXTERNAL_ACCESS_INTEGRATIONS = (<literal EAI name>) fail at call time under that model, but SPCS service specs support EXTERNAL_ACCESS_INTEGRATIONS = (REFERENCE('zeotap_sf_api_access')). Routing the registration handshake through the SPCS service is the only marketplace-compliant path for the outbound call.
The setup app’s Register with Zeotap button calls APP_DATA.REQUEST_REGISTRATION(<your-email>), which writes a single row to APP_DATA.REGISTRATION_REQUEST. The control-plane service polls that table, signs the body with the keypair from APP_DATA.CLOUD_CREDENTIALS, POSTs to the cloud’s /api/v1/native-apps/register endpoint through the EAI reference, and writes the cloud-issued claim URL into APP_DATA.CLOUD_REGISTRATION. The setup app polls both tables and surfaces the URL as soon as it appears — typically within three to six seconds.
The flow is idempotent: re-running REQUEST_REGISTRATION is safe, and the cloud’s re-registration branch updates ingress_url, auth_credential, and status in place using the stored register_secret for signing. Useful after rotating the keypair or recovering from a transient failure.
Why this shape
The split between long-running services and ephemeral job services is what keeps the steady-state SPCS bill predictable. The four long-running services use a fixed compute pool node; sync, loader, identity, and computed attribute work — which is bursty and concurrent — fans out to job services that exit when the run completes. The block volume on Postgres is the only persistent storage; everything else is rehydrated from Postgres on service restart.
The reverse-proxy chain rather than a direct browser-to-SPCS connection is what lets Zeotap ship the same UI to cloud and Native App customers. The browser doesn’t know whether the workspace it’s looking at runs in the cloud or in an SPCS install — both look like normal composable.zeotap.com API calls.
The application-owned warehouse keeps the install self-contained. Earlier versions bound a consumer-supplied customer_warehouse reference, which required the consumer to pre-create a warehouse, bind it in the application’s Set up tile before provisioning could proceed, and run a separate GRANT USAGE, OPERATE ON WAREHOUSE ... TO APPLICATION block. Owning the warehouse internally removes those steps and lets the application self-manage size (SET_WAREHOUSE_SIZE), auto-suspend, and lifecycle. The consumer still pays for the credits the warehouse consumes — billed against their Snowflake account — but does not have to provision or grant anything to make it run.