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, andCREATE DATABASEaccount 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.