D1 to Astro Flow
How data flows from Cloudflare D1 through the Astro SSR site at vinny.vegas
1 — Full Pipeline
graph TD A["Scraper
VegasEvent Data"] --> B["Image Downloader"] B --> C["R2 Upload
img.vinny.vegas"] C --> D["D1 Export
SQL Schema"] D --> E["D1 Database
5 Tables"] E --> F["Astro SSR
Query Helpers"] F --> G["Cloudflare Pages
Deploy"] G --> H["Browser
vinny.vegas"] classDef scraper fill:#38bdf811,stroke:#38bdf844,stroke-width:1.5px classDef r2 fill:#fb923c11,stroke:#fb923c44,stroke-width:1.5px classDef d1 fill:#a78bfa11,stroke:#a78bfa44,stroke-width:1.5px classDef astro fill:#34d39911,stroke:#34d39944,stroke-width:1.5px classDef browser fill:#fb718511,stroke:#fb718544,stroke-width:2px class A scraper class B,C r2 class D,E d1 class F,G astro class H browser
Pipeline order matters: Images must be uploaded to R2 before D1 export, so that
artist_image_url is populated in the database. All SQL is imported as a single transaction—ALTER TABLE failures abort the entire import.
2 — D1 Schema
5 relational tables with composite keys for deduplication:
events
Main event records
- composite_key (PK): unique event identifier
- date, title, description
- performer, venue, venue_id
- pricing_tiers (JSON)
- created_at, updated_at
artists
Enriched artist profiles
- artist_slug (PK)
- name, bio, image_url
- resident_advisor_url
- spotify_id, top_tracks (JSON)
- enrichment_status (JSON)
event_artists
Event-artist junction
- composite_key (FK → events)
- artist_slug (FK → artists)
- role (headliner, opener, resident...)
- PK: (composite_key, artist_slug)
images
Artist images from R2
- artist_slug (FK)
- size (main, hd)
- venue_tag (ebc, xs, liv, etc.)
- image_url (R2 CDN link)
- PK: (artist_slug, size, venue_tag)
table_tiers
Table pricing tiers
- composite_key (FK → events)
- tier_name, min_spend
- guest_count, table_count
- notes (JSON)
3 — Import Methods
Two paths to D1:
| Method | How It Works | Requirements | Best For |
|---|---|---|---|
| REST API Preferred |
Direct POST /query endpoint with Bearer token |
CLOUDFLARE_API_TOKEN with D1 Edit permission |
CI/CD pipelines, production deploys |
| Wrangler Fallback |
Subprocess: wrangler d1 execute with local config |
wrangler.jsonc with database_id + database_name |
Local dev, when API token unavailable |
Batching Strategy:
- SQL split on
;delimiters - Sent in batches of 100 statements
- Each batch: one HTTP request or wrangler call
- Parallel execution: up to 5 concurrent batches
- Retries on transient failures (up to 3 attempts)
No ALTER TABLE. All columns must be defined in CREATE TABLE statements. If you need schema changes, drop and recreate the table.
4 — Composite Key Strategy
The composite_key is a content-addressed primary key, not an external EVE ID:
composite_key = hash(venue_id + performer + date) Example: "e3b0c44298fc1c149afbf4c8996fb924"
Why not use EVE IDs?
- Venue independence: Same performer at different venues = different composite keys
- Deduplication: Automatically detect duplicate scrapes (same hash)
- Stability: EVE IDs are external and sometimes unstable; composite keys are stable across re-scrapes
- Simplicity: No external ID mappings required
Dedup in Action:
Event 1: LIV Las Vegas + "Carl Cox" + "2026-03-15" → composite_key = "abc123def456" Event 2: XS Nightclub + "Carl Cox" + "2026-03-15" → composite_key = "xyz789uvw012" ← different venue Event 3: LIV Las Vegas + "Carl Cox" + "2026-03-15" (re-scrape) → composite_key = "abc123def456" ← same, skip insert
5 — Astro SSR Architecture
Astro 6 SSR with Cloudflare adapter and D1 binding:
graph LR A["D1
Database"] --> B["Astro SSR
Route Handler"] B --> C["Query Helpers
src/lib/queries.ts"] C --> D["Template
.astro file"] D --> E["Cloudflare Pages
Edge Runtime"] E --> F["Browser
vinny.vegas"] classDef db fill:#38bdf811,stroke:#38bdf844,stroke-width:1.5px classDef handler fill:#fb923c11,stroke:#fb923c44,stroke-width:1.5px classDef page fill:#a78bfa11,stroke:#a78bfa44,stroke-width:1.5px classDef browser fill:#fb718511,stroke:#fb718544,stroke-width:2px class A db class B,C handler class D,E page class F browser
Key Components:
D1 Binding
Astro.locals.runtime.env.DB — injected by Cloudflare adapterQuery Helpers
Prepared statements in
src/lib/queries.ts — parameterized to prevent SQL injectionRoute Handlers
src/pages/ — Astro routes, each can query D1 in getStaticProps or API handlersDeployment
Push to GitLab → Cloudflare Pages → auto-builds and deploys on main branch
6 — Routes & Query Helpers
Astro routes and their corresponding D1 queries:
| Route | Purpose | Query Helper | Key Tables |
|---|---|---|---|
| / | Homepage with upcoming events list | getUpcomingEvents() |
events, artists, images |
| /events | All events with filters (date, venue) | queryEvents(filters) |
events, event_artists |
| /events/[key] | Single event detail page | getEventByKey(key) |
events, event_artists, table_tiers |
| /venues | All venues (LIV, XS, EBC, TAO...) | getVenues() |
events (group by venue) |
| /venues/[id] | Single venue with its events | getVenueEvents(venueId) |
events filtered by venue_id |
| /artists/[slug] | Artist bio with appearances | getArtistBySlug(slug) |
artists, event_artists, images |
| /tables | Table pricing browse/filter | getTableTiers(filters) |
table_tiers, events |
Query Helper Pattern:
export async function getEventByKey(db, key) {
const stmt = db.prepare(
`SELECT * FROM events
WHERE composite_key = ?1`
);
return stmt.bind(key).first();
}
7 — Null Safety & SSR Gotchas
SSR Silent Failure: When template code throws during server-side render, Astro silently renders an empty
<main> with HTTP 200. Dev console shows stack trace; production does not. This is the #1 debugging gotcha.
Safe Helpers in src/lib/safe.ts:
- safeFormatDate(iso) — wraps
format(parseISO(...)), returns empty string on error - safeParseJsonObject(str) — wraps
JSON.parse(str), returns{}on parse error or null input - safeInitial(str) — safely accesses
str[0], checks length first
JSON.parse("null") gotcha: This is valid JSON that returns JS
null. Never assume JSON.parse() returns an object. Always use safeParseJsonObject().
Common SSR Triggers:
- Calling
format(parseISO(null))on null date from D1 Object.keys(JSON.parse("null"))— crashes because JS null has no properties- Accessing
str[0]on null artist name - Template iteration over potentially undefined arrays
XS Nightclub = best canary. XS has the sparsest data (minimal enrichment, no images for many artists). Always test new templates against XS events first.
8 — Troubleshooting
Common Errors:
| Error | Cause | Fix |
|---|---|---|
| 403 Forbidden (from pricing API) |
Missing User-Agent header on HTTP request |
Add header: {"User-Agent": "Mozilla/5.0..."} |
| no such table | D1 import failed silently (check wrangler logs) | Re-run import with --force, check SQL syntax |
| empty <main> on routes |
SSR template threw exception | Check dev logs, use safe.ts helpers, test with XS events |
| artist_image_url null | Images uploaded after D1 export | Upload images first, then export D1 (check pipeline order) |
| composite_key collision | Hash function produced same key for different events | Verify venue_id + performer + date uniqueness before insert |
Debug Checklist:
- Is D1 database reachable?
wrangler d1 execute --command "SELECT COUNT(*) FROM events;" - Are all 5 tables created?
.schemain D1 CLI - Do events have composite_keys? Query
SELECT COUNT(composite_key) FROM events; - Are images uploaded to R2? Check
img.vinny.vegasbucket - Is
R2_PUBLIC_URLset in wrangler.toml? - Check Cloudflare Pages build logs for SSR errors
- Use XS Nightclub as test venue (sparsest data, catches rendering bugs)