Integrate Tableflow with Snowflake Horizon Catalog in Confluent Cloud

Snowflake Horizon Catalog is the recommended way to consume Tableflow tables in Snowflake. The integration works through catalog federation.

Snowflake uses a catalog-linked database to connect directly to the Tableflow Apache Iceberg™ REST Catalog. It automatically discovers every Bring Your Own Storage (BYOS) Tableflow table in an Apache Kafka® cluster and reads the data from your storage bucket. Snowflake treats Tableflow tables as read-only. You don’t configure anything in Tableflow, Tableflow doesn’t copy metadata into a second catalog, and Snowflake needs no per-table setup.

This approach supersedes Snowflake Open Catalog synchronization for new deployments. Snowflake no longer offers Open Catalog sign-up to new accounts and directs them to Horizon Catalog. For more information, see the Snowflake Open Catalog overview.

How the integration works

  • A Snowflake catalog integration object connects to the Tableflow REST Catalog endpoint, authenticating with a Tableflow-scoped Confluent Cloud API key by using the OAuth 2.0 client credentials flow. Snowflake obtains and refreshes tokens automatically.

  • A Snowflake catalog-linked database built on that integration polls the catalog, with a default interval of 30 seconds. The Kafka cluster appears as a schema, and every BYOS Tableflow table materializes as an externally managed Iceberg table. Newly enabled topics appear automatically.

  • Snowflake reads data from your BYOS bucket through a read-only external volume backed by an IAM role that you own. The Tableflow REST Catalog doesn’t vend storage credentials for BYOS buckets, so you must create the external volume.

  • Tableflow remains the only writer and performs all table maintenance, including compaction and snapshot management. Snowflake is a zero-copy, read-only consumer.

Prerequisites

  • A Tableflow-enabled topic in Iceberg format with status Running, stored in a BYOS Amazon S3 bucket. Horizon Catalog can’t federate topics on Confluent-managed storage, and these steps don’t cover Azure Data Lake Storage.

  • A Tableflow API key, created with confluent api-key create --resource tableflow. The key’s principal must have permission to list and read every topic you want Snowflake to discover. For more information, see Grant Role-Based Access for Tableflow in Confluent Cloud.

  • A Snowflake role with the CREATE INTEGRATION, CREATE EXTERNAL VOLUME, and CREATE DATABASE account privileges.

  • Permission to create an IAM policy and role in the AWS account that owns the BYOS bucket.

Step 1: Create an IAM policy for the BYOS bucket

In the AWS account that owns the bucket, create a read-only IAM policy. In the AWS console, navigate to IAM > Policies > Create policy > JSON, and save the following policy as snowflake-tableflow-read-policy. If the bucket uses SSE-KMS, also allow kms:Decrypt on the bucket’s KMS key.

{
  "Version": "2012-10-17",
  "Statement": [
    {"Effect": "Allow", "Action": ["s3:GetObject", "s3:GetObjectVersion"],
     "Resource": "arn:aws:s3:::<bucket>/*"},
    {"Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
     "Resource": "arn:aws:s3:::<bucket>"}
  ]
}

Step 2: Create the IAM role

In the AWS console, navigate to IAM > Roles > Create role > Custom trust policy. Start with a placeholder trust policy that trusts your own account. You replace the principal in Step 4. Attach the policy from Step 1 and name the role snowflake-tableflow-read-role. Choose your external ID now. Use a unique, hard-to-guess value, such as a UUID, to protect the role against confused-deputy access. Pinning the external ID enables you to author the final trust policy in one pass.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"AWS": "arn:aws:iam::<aws-account-id>:root"},
    "Action": "sts:AssumeRole",
    "Condition": {"StringEquals": {"sts:ExternalId": "<pinned-external-id>"}}
  }]
}

Step 3: Create the external volume in Snowflake

Point STORAGE_BASE_URL at the bucket root so one volume covers every current and future Tableflow table in the bucket. The catalog-linked database validates every table against this base path, so store every BYOS Tableflow table in the Kafka cluster in this bucket. Keep ALLOW_WRITES = FALSE.

CREATE EXTERNAL VOLUME tableflow_byos_vol
  STORAGE_LOCATIONS = ((
    NAME = '<bucket>-byos',
    STORAGE_PROVIDER = 'S3',
    STORAGE_BASE_URL = 's3://<bucket>/',
    STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::<aws-account-id>:role/snowflake-tableflow-read-role',
    STORAGE_AWS_EXTERNAL_ID = '<pinned-external-id>'
  ))
  ALLOW_WRITES = FALSE;

DESCRIBE EXTERNAL VOLUME tableflow_byos_vol;

From the DESCRIBE output, note the STORAGE_AWS_IAM_USER_ARN value for the trust policy in the next step.

Step 4: Grant Snowflake access to the IAM role

In AWS, edit the trust relationship of snowflake-tableflow-read-role and replace the placeholder principal with the STORAGE_AWS_IAM_USER_ARN value from Step 3, keeping the pinned external ID.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"AWS": "<STORAGE_AWS_IAM_USER_ARN>"},
    "Action": "sts:AssumeRole",
    "Condition": {"StringEquals": {"sts:ExternalId": "<pinned-external-id>"}}
  }]
}

Verify the data path from Snowflake. Because the volume doesn’t allow writes, the read and write sub-checks report UNVERIFIED. You can ignore these results.

SELECT SYSTEM$VERIFY_EXTERNAL_VOLUME('tableflow_byos_vol');

Step 5: Create the catalog integration

Use ACCESS_DELEGATION_MODE = EXTERNAL_VOLUME_CREDENTIALS. Don’t use VENDED_CREDENTIALS. The Tableflow REST Catalog doesn’t vend storage credentials to Snowflake, so that mode passes verification but fails at database creation. You can’t change the mode on an existing integration, so if you created one with VENDED_CREDENTIALS, drop it and create a new one.

The Kafka cluster ID is the catalog namespace. Copy <region>, <org-id>, and <env-id> from the REST Catalog Endpoint shown in the API access section of the Tableflow page in Confluent Cloud Console. OAUTH_TOKEN_URI is that same endpoint with /v1/oauth/tokens appended.

CREATE CATALOG INTEGRATION tableflow_horizon_int
  CATALOG_SOURCE = ICEBERG_REST
  TABLE_FORMAT = ICEBERG
  CATALOG_NAMESPACE = '<kafka-cluster-id>'
  REST_CONFIG = (
    CATALOG_URI = 'https://tableflow.<region>.aws.confluent.cloud/iceberg/catalog/organizations/<org-id>/environments/<env-id>',
    CATALOG_API_TYPE = PUBLIC,
    ACCESS_DELEGATION_MODE = EXTERNAL_VOLUME_CREDENTIALS
  )
  REST_AUTHENTICATION = (
    TYPE = OAUTH,
    OAUTH_TOKEN_URI = 'https://tableflow.<region>.aws.confluent.cloud/iceberg/catalog/organizations/<org-id>/environments/<env-id>/v1/oauth/tokens',
    OAUTH_CLIENT_ID = '<tableflow-api-key>',
    OAUTH_CLIENT_SECRET = '<tableflow-api-secret>',
    OAUTH_ALLOWED_SCOPES = ('catalog')
  )
  ENABLED = TRUE;

SELECT SYSTEM$VERIFY_CATALOG_INTEGRATION('tableflow_horizon_int');

The verification returns "success" : true when the connection works.

Step 6: Create the catalog-linked database

Create one linked database per Kafka cluster, scoped to that cluster’s namespace. The statement validates live against the catalog and the volume, so allow 30 to 60 seconds.

CREATE DATABASE tableflow_linked_db
  LINKED_CATALOG = (
    CATALOG = 'tableflow_horizon_int',
    ALLOWED_NAMESPACES = ('<kafka-cluster-id>'),
    SYNC_INTERVAL_SECONDS = 30
  )
  EXTERNAL_VOLUME = 'tableflow_byos_vol';

SELECT SYSTEM$CATALOG_LINK_STATUS('tableflow_linked_db');
SHOW ICEBERG TABLES IN DATABASE tableflow_linked_db;

Every BYOS Tableflow table in the cluster that’s stored under the volume’s base path materializes within a sync cycle.

Query the tables

Tables surface under a schema named after the Kafka cluster ID, one table per topic. User-defined namespaces configured for external catalog integrations don’t apply, because Horizon Catalog reads the Tableflow Iceberg REST Catalog directly. Snowflake uppercases unquoted identifiers, so quote the namespace and table name exactly as they appear in Tableflow.

SELECT * FROM tableflow_linked_db."<kafka-cluster-id>"."<topic-name>" LIMIT 100;

A newly enabled topic shows its schema after the next catalog sync but returns rows only after the first Tableflow data commit. From then on, freshness tracks the Tableflow commit cadence plus the sync interval. For more information, see Query Iceberg Tables with Snowflake.

Limitations

  • BYOS only: tables on Confluent-managed storage can’t be consumed through this method, because you can’t grant IAM access to Confluent-owned buckets, and the catalog doesn’t vend credentials to Snowflake.

  • One bucket per catalog-linked database: Snowflake validates every table against the external volume’s base path, so store every BYOS Tableflow table in the Kafka cluster in this bucket.

  • Read-only in Snowflake: the federated tables are externally managed, with no cloning, no materialized views on tables protected by row access policies, and no DML.

Snowflake governance features, including masking policies, row access policies, and tags, apply to the federated tables inside Snowflake.