Skip to content

Commit bd7d4f4

Browse files
lw15 d2 (supabase#37148)
* d2 init * LW15 Analytics Buckets Blog Post (supabase#37131) * lw-15 analytics bucket blog post * polish * og and thumb --------- Co-authored-by: Francesco Sansalvadore <[email protected]> * add nav to mobile main stage slider * d2 data * fix nit * add video id * add author * rss --------- Co-authored-by: Oliver Rice <[email protected]>
1 parent 28dbc00 commit bd7d4f4

File tree

16 files changed

+238
-44
lines changed

16 files changed

+238
-44
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
---
2+
title: 'Supabase Analytics Buckets with Iceberg Support'
3+
description: 'Analytics buckets optimized for large-scale data analysis with Apache Iceberg support.'
4+
categories:
5+
- product
6+
- launch-week
7+
tags:
8+
- launch-week
9+
- storage
10+
date: '2025-07-15:10:00'
11+
toc_depth: 3
12+
author: oli_rice,fabrizio
13+
image: launch-week-15/day-2-analytics-buckets/og.jpg
14+
thumb: launch-week-15/day-2-analytics-buckets/thumb.png
15+
launchweek: 15
16+
---
17+
18+
Today we're launching **Supabase Analytics Buckets** in private alpha. These are a new kind of storage bucket optimized for analytics, with built-in support for the [Apache Iceberg](https://iceberg.apache.org/) table format.
19+
20+
Analytics buckets are integrated into Supabase Studio, power table-level views instead of raw files, and can be queried using the new **Supabase Iceberg Wrapper**, also launching in alpha.
21+
22+
<div className="video-container mb-8">
23+
<iframe
24+
className="w-full"
25+
src="https://www.youtube-nocookie.com/embed/BigtFoFCVBk"
26+
title="Supabase Analytics Buckets with Iceberg Support"
27+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share"
28+
allowfullscreen
29+
/>
30+
</div>
31+
32+
## Why Iceberg
33+
34+
Apache Iceberg is a high-performance, open table format for large-scale analytics on object storage. It brings the performance and features of a database to the flexibility of flat files.
35+
36+
We chose Iceberg for its bottomless data model (append-only, immutable history), built-in snapshotting and versioning (time travel), and support for schema evolution. Iceberg is also an open standard widely supported across the ecosystem. Supabase is committed to [**open standards and portability**](https://supabase.com/blog/open-data-standards-postgres-otel-iceberg), and Iceberg aligns with that goal by enabling users to move data in and out without being locked into proprietary formats.
37+
38+
## Setting up Analytics Buckets
39+
40+
Once your project has been accepted into the alpha release program, Analytics buckets can be created via Studio and the API. To create an analytics bucket, visit `Storage > New bucket` in Studio.
41+
42+
<Img
43+
alt="Creating a new analytics bucket"
44+
src="/images/blog/launch-week-15/day-2-analytics-buckets/img1.png"
45+
/>
46+
47+
Analytics buckets are a separate bucket type from standard Supabase Storage buckets. You can't mix file types between the two.
48+
49+
They're stored in a new system table: `storage.buckets_iceberg`. These buckets are not included in the `storage.buckets` table and objects inside them are not shown in `storage.objects`. However, the `listBuckets()` endpoint returns a merged list of standard and analytics buckets for consistency with Studio and API consumers.
50+
51+
<Img
52+
alt="Analytics buckets in Studio"
53+
src="/images/blog/launch-week-15/day-2-analytics-buckets/img4.png"
54+
/>
55+
56+
After creating the bucket, we're met with connection details. Copy the `WAREHOUSE`, `VAULT_TOKEN`, and `CATALOG_URI` values and and create an Iceberg namespace and table using your preferred method. The example below uses pyiceberg to create a namespace `market` with table `prices`:
57+
58+
```python
59+
import datetime
60+
import pyarrow as pa
61+
from pyiceberg.catalog.rest import RestCatalog
62+
from pyiceberg.exceptions import NamespaceAlreadyExistsError, TableAlreadyExistsError
63+
64+
# Define catalog connection details (replace variables)
65+
WAREHOUSE= ...
66+
VAULT_TOKEN = ...
67+
CATALOG_URI= ...
68+
69+
# Connect to Supabase Data Catalog
70+
catalog = RestCatalog(
71+
name="catalog",
72+
warehouse=WAREHOUSE,
73+
uri=CATALOG_URI,
74+
token=VAULT_TOKEN,
75+
)
76+
77+
# Schema and Table Names
78+
namespace_name = "market"
79+
table_name = "prices"
80+
81+
# Create default namespace
82+
catalog.create_namespace(namespace_name)
83+
84+
df = pa.table({
85+
"tenant_id": pa.array([], type=pa.string()),
86+
"store_id": pa.array([], type=pa.string()),
87+
"item_id": pa.array([], type=pa.string()),
88+
"price": pa.array([], type=pa.float64()),
89+
"timestamp": pa.array([], type=pa.int64()),
90+
})
91+
92+
# Create an Iceberg table
93+
table = catalog.create_table(
94+
(namespace_name, table_name),
95+
schema=df.schema,
96+
)
97+
```
98+
99+
Back in Studio, we can see the newly created our newly created Namespace with `0/1 connected tables`
100+
101+
<Img
102+
alt="Iceberg namespace in Studio"
103+
src="/images/blog/launch-week-15/day-2-analytics-buckets/img5.png"
104+
/>
105+
106+
Click connect and select a `Target Schema` to map the Iceberg tables into. It is reccomended to create a standalone schema for your tables. Do not use the `public` schema because that would expose your table over the project's REST API.
107+
108+
<Img
109+
alt="Connecting Iceberg tables to schema"
110+
src="/images/blog/launch-week-15/day-2-analytics-buckets/img2.png"
111+
/>
112+
113+
## Querying Analytics Buckets
114+
115+
Viewing an analytics bucket in Supabase Studio redirects you to the Table Editor. Instead of exposing raw Parquet files, the system shows a table explorer, powered by the [**Supabase Iceberg Wrapper**](https://fdw.dev/catalog/).
116+
117+
The wrapper exposes Iceberg tables through a SQL interface, so you can inspect and query your data using Studio, or any SQL IDE. This makes analytical data feel like a native part of your Supabase project.
118+
119+
<Img
120+
alt="Querying Iceberg tables in Studio"
121+
src="/images/blog/launch-week-15/day-2-analytics-buckets/img3.png"
122+
/>
123+
124+
In this case the corresponding SQL query to access the data would be
125+
126+
```sql
127+
select
128+
*
129+
from market_analytics.prices;
130+
```
131+
132+
## Writing to Analytics Buckets
133+
134+
Writing is a work in progress. We're actively building [**Supabase ETL**](https://github.com/supabase/etl), which will allow you to write directly from Postgres into Iceberg-backed buckets. We'll also add write capability to the Supabase Iceberg Wrapper as soon as write support lands in the upstream [iceberg-rust client library](https://github.com/apache/iceberg-rust). This will complete the workflow of **write → store → query**, all inside Supabase.
135+
136+
Once live, that enables bottomless Postgres storage through shifting records into Analytics Buckets, all using open formats. As a bonus, Iceberg gets us time travel for free.
137+
138+
## Alpha Launch Limits
139+
140+
Analytics Buckets are launching in private alpha with the following constraints:
141+
142+
- Two analytics buckets per project
143+
- Up to five namespaces per bucket
144+
- Ten tables per namespace
145+
- Pricing will be announced in a few weeks
146+
- You cannot store standard objects in analytics buckets
147+
148+
## Roadmap and What's Next
149+
150+
This launch marks the first step toward full analytical capabilities in Supabase. Over the next few months, we'll introduce SQL catalog support so you can explore Iceberg table metadata directly from the database. Studio will also gain deeper integration for schema inspection, column-level filtering, and time travel queries. Our goal is to make Supabase a full-featured HTAP backend, where you can write, store, and query analytical data seamlessly.
151+
152+
## Try It Out
153+
154+
[Join the waitlist here](https://forms.supabase.com/analytics-buckets) to get early access and start working with bottomless, time-travel-capable analytics data inside Supabase.

apps/www/components/Hero/Hero.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const Hero = () => {
1919
<AnnouncementBadge
2020
url="/launch-week#main-stage"
2121
badge="LW15"
22-
announcement={announcement.launch}
22+
announcement={`Day 2: ${announcement.launch}`}
2323
className="lg:-mt-8"
2424
hasArrow
2525
/>

apps/www/components/LaunchWeek/15/LW15Heading.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ const LW15Heading = () => {
113113
<div
114114
data-animate
115115
data-animate-delay={300}
116-
className="hidden md:inline-block opacity-0 col-span-3 lg:col-span-2 text-xs overflow-hidden h-fit max-w-[400px]"
116+
className="opacity-0 col-span-3 lg:col-span-2 text-xs overflow-hidden h-fit max-w-[400px]"
117117
>
118118
Five days of launches to supercharge your development.
119119
</div>

apps/www/components/LaunchWeek/15/LW15MainStage.tsx

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
import 'swiper/css'
2+
import 'swiper/css/a11y'
3+
import 'swiper/css/navigation'
4+
import 'swiper/css/controller'
25
import React, { useEffect, useRef, useState } from 'react'
36
import Image from 'next/image'
47
import Link from 'next/link'
58
import { Button, cn } from 'ui'
69
import { useTheme } from 'next-themes'
7-
import { Swiper, SwiperSlide } from 'swiper/react'
10+
import { Swiper, SwiperClass, SwiperRef, SwiperSlide } from 'swiper/react'
11+
import { Controller, Navigation, A11y } from 'swiper/modules'
812

913
import SectionContainer from 'components/Layouts/SectionContainer'
1014
import { mainDays, WeekDayProps } from './data'
1115
import { useWindowSize } from 'react-use'
1216
import { useBreakpoint } from 'common'
1317
import { DayLink } from './lw15.components'
18+
import { ChevronLeft, ChevronRight } from 'lucide-react'
1419

1520
const LW15MainStage = ({ className }: { className?: string }) => {
1621
const { resolvedTheme } = useTheme()
1722
const isDark = resolvedTheme?.includes('dark')
23+
const swiperRef = useRef<SwiperRef>(null)
24+
const [controlledSwiper, setControlledSwiper] = useState<SwiperClass | null>(null)
1825
const days = mainDays(isDark!)
1926

2027
return (
@@ -26,14 +33,36 @@ const LW15MainStage = ({ className }: { className?: string }) => {
2633
)}
2734
id="main-stage"
2835
>
29-
<h3 className="text-2xl lg:text-3xl">Main Stage</h3>
36+
<div className="flex justify-between items-center">
37+
<h3 className="text-2xl lg:text-3xl">Main Stage</h3>
38+
<div className="flex xl:hidden items-center gap-2 text-foreground-muted">
39+
<button
40+
onClick={() => controlledSwiper?.slidePrev()}
41+
className="p-2 rounded-full hover:text-foreground border hover:border-foreground transition-colors"
42+
>
43+
<ChevronLeft className="w-4 h-4 -translate-x-px text-current" />
44+
</button>
45+
<button
46+
onClick={() => controlledSwiper?.slideNext()}
47+
className="p-2 rounded-full hover:text-foreground border hover:border-foreground transition-colors"
48+
>
49+
<ChevronRight className="w-4 h-4 translate-x-px text-current" />
50+
</button>
51+
</div>
52+
</div>
3053
<div className="hidden xl:flex flex-nowrap justify-between gap-2">
3154
{days.map((day) => (
3255
<DayCard day={day} key={day.dd} />
3356
))}
3457
</div>
3558
</SectionContainer>
36-
<CardsSlider slides={days} className="xl:hidden" />
59+
<CardsSlider
60+
slides={days}
61+
className="xl:hidden"
62+
swiperRef={swiperRef}
63+
setControlledSwiper={setControlledSwiper}
64+
controlledSwiper={controlledSwiper}
65+
/>
3766
</div>
3867
)
3968
}
@@ -49,7 +78,7 @@ const DayCard = ({ day }: { day: WeekDayProps }) =>
4978
>
5079
<div className="w-full h-full relative z-10 flex flex-col justify-between gap-4">
5180
<div></div>
52-
<div className="flex flex-col gap-2 p-4">
81+
<div className="flex flex-col gap-2 p-4 pt-0">
5382
<span className="text-xl text-foreground-lighter">{day.date}</span>
5483
<span className="text-base leading-snug text-foreground-muted">
5584
&#91; Access locked &#93;
@@ -82,7 +111,7 @@ const DayCardShipped = ({ day }: { day: WeekDayProps }) => {
82111
>
83112
<CardBG day={day} />
84113
<div className="w-full h-full relative z-10 flex flex-col justify-between gap-4 overflow-hidden">
85-
<ul className="flex flex-col gap-1 p-4">
114+
<ul className="flex flex-col gap-1 p-4 pb-0 lg:opacity-0 lg:blur-lg duration-300 group-hover/main:lg:blur-none transition-all group-hover/main:lg:opacity-100">
86115
{day.links?.map((link) => (
87116
<li key={link.href}>
88117
<DayLink
@@ -93,7 +122,7 @@ const DayCardShipped = ({ day }: { day: WeekDayProps }) => {
93122
))}
94123
</ul>
95124
<div
96-
className="flex flex-col p-4 gap-2 relative group-hover/main:!bottom-0 !ease-[.25,.25,0,1] duration-300"
125+
className="flex flex-col p-4 pt-0 gap-2 relative group-hover/main:!bottom-0 !ease-[.25,.25,0,1] duration-300"
97126
style={{
98127
bottom: isTablet ? 0 : -hiddenHeight + 'px',
99128
}}
@@ -152,16 +181,31 @@ const CardBG = ({ day }: { day: WeekDayProps }) => (
152181
interface Props {
153182
className?: string
154183
slides: WeekDayProps[]
184+
swiperRef: React.RefObject<SwiperRef>
185+
setControlledSwiper: (swiper: SwiperClass) => void
186+
controlledSwiper: SwiperClass | null
155187
}
156188

157-
const CardsSlider: React.FC<Props> = ({ slides, className }) => (
189+
const CardsSlider: React.FC<Props> = ({
190+
slides,
191+
className,
192+
swiperRef,
193+
setControlledSwiper,
194+
controlledSwiper,
195+
}) => (
158196
<div className={cn('relative lg:container mx-auto px-6 lg:px-16', className)}>
159197
<Swiper
160-
initialSlide={0}
198+
ref={swiperRef}
199+
onSwiper={setControlledSwiper}
200+
modules={[Controller, Navigation, A11y]}
201+
initialSlide={1}
161202
spaceBetween={8}
162203
slidesPerView={1.5}
163204
breakpoints={{
164-
540: {
205+
520: {
206+
slidesPerView: 1.9,
207+
},
208+
640: {
165209
slidesPerView: 2.5,
166210
},
167211
720: {
@@ -174,6 +218,7 @@ const CardsSlider: React.FC<Props> = ({ slides, className }) => (
174218
speed={400}
175219
watchOverflow
176220
threshold={2}
221+
controller={{ control: controlledSwiper }}
177222
updateOnWindowResize
178223
allowTouchMove
179224
className="!w-full !overflow-visible"

0 commit comments

Comments
 (0)