📼 https://youtu.be/pkS1ccQBoqg
LinkedOut is an educational demo application that showcases Cloudflare's modern data stack for building real-time analytics applications with authenticated multi-user collaboration.
This project demonstrates a complete data pipeline using:
- Workers - Serverless compute for handling HTTP requests
- D1 - Serverless SQL database for relational data
- Pipelines - Stream ingestion for real-time event data
- R2 Data Catalog - Apache Iceberg tables for structured analytics data
- R2 SQL - Query analytics data with standard SQL
- Cloudflare Access - Zero Trust authentication with GitHub OAuth
LinkedOut allows educators and presenters to share links after talks and track engagement:
- Create markdown pages with links to share
- Collaborate with co-presenters via multi-maintainer system
- Customize with beautiful pre-built themes
- Share via URL or QR code
- Track page views, link clicks, and QR scans
- Analyze engagement data with SQL queries
┌─────────────────────────────────────────────────────────────────┐
│ User Actions │
│ - Visits /out/my-talk │
│ - Clicks link to external site │
│ - Scans QR code │
└────────────────┬────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare Worker │
│ - Authenticates via Cloudflare Access (GitHub) │
│ - Serves pages from D1 database │
│ - Receives tracking events via sendBeacon │
│ - Writes events to Pipeline stream │
└────────────────┬────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ Pipelines (Stream) │
│ - Batches events for efficient writes │
│ - Applies SQL transformations (SELECT * FROM stream) │
└────────────────┬────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ R2 Data Catalog (Sink) │
│ - Writes to Apache Iceberg table │
│ - Stores as Parquet files in R2 │
│ - Maintains Iceberg metadata for queries │
└────────────────┬────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ R2 SQL │
│ - Query data with standard SQL │
│ - Aggregations, filters, time-series analysis │
│ - Powers analytics dashboard │
└─────────────────────────────────────────────────────────────────┘
- Cloudflare Access - GitHub OAuth authentication
- Zero Trust - JWT-based auth, no passwords to manage
- Admin Panel - User management and promotion system
- Multi-User - Role-based access control
- Multi-Maintainer - Share link ownership with co-presenters
- Equal Permissions - All maintainers can edit and manage links
- Team Links - Perfect for conference talks with multiple speakers
- 6 Pre-built Themes - Default, Dark, Minimal, Colorful, Conference, Retro
- CSS Variables - 10 customizable variables per theme
- Theme Selection - Choose different themes for different links
- Public/Private Themes - Share themes or keep them private
- Real-Time Tracking - Page views, clicks, QR scans
- Geographic Data - Country, city, region from Cloudflare
- Interactive Map - Global visualization of page views
- Link Performance - Click-through rates, destination URLs, link text
- Filterable - View all activity or filter by specific links
- QR Code Generation - Automatic QR codes for each page
- Hotkeys - Press 'Q' to toggle QR modal
- Download - Export QR codes as PNG
- Tracking - Separate QR scan tracking
See QUICKSTART.md for a quick guide, or follow the detailed setup below.
- Node.js 18+
- Cloudflare account
- Wrangler CLI:
npm install -g wrangler
# Clone repository
git clone https://github.com/yourusername/linkedout-pipelines
cd linkedout-pipelines
# Install dependencies
npm install
# Login to Cloudflare
npx wrangler login# Create D1 database (if not exists)
npx wrangler d1 create linkedout-db
# Apply migrations
npx wrangler d1 migrations apply linkedout-db --remote
# Create your first admin user
npx wrangler d1 execute linkedout-db --remote \
--command "INSERT INTO users (email, is_admin, created_at) VALUES ('your@email.com', 1, datetime('now'))"- Go to Zero Trust Dashboard → Access → Applications
- Click Add an Application → Self-hosted
- Configure:
- Application domain:
your-worker.workers.dev(or custom domain) - Path:
/dashboard(or/dashboard/*for subpaths)
- Application domain:
- Add GitHub as an identity provider
- Create a policy to allow authenticated users
- Important: Enable "Send Cf-Access-JWT-Assertion header"
# Set your R2 API token
export R2_API_TOKEN="your_token_here"
# Create stream
npx wrangler pipelines streams create click_events_v6 \
--schema-file schema.json \
--http-enabled true \
--http-auth false
# Create sink
npx wrangler pipelines sinks create click_events_sink_v6 \
--type r2-data-catalog \
--bucket linkedout-data-catalog \
--namespace default \
--table click_events_v6 \
--catalog-token "$R2_API_TOKEN" \
--compression zstd \
--roll-size 100 \
--roll-interval 300
# Create pipeline
npx wrangler pipelines create click_events_pipeline_v6 \
--sql "INSERT INTO click_events_sink_v6 SELECT * FROM click_events_v6"
# Note the pipeline ID and update wrangler.jsoncnpm run dev # Start dev server at localhost:8787
npm test # Run tests
npm run deploy # Deploy to productionlinkedout-pipelines/
├── src/
│ ├── index.tsx # Main Worker (Hono framework)
│ ├── types.ts # TypeScript interfaces
│ ├── middleware/
│ │ └── auth.ts # Cloudflare Access auth middleware
│ ├── routes/
│ │ ├── dashboard.ts # Dashboard & link management
│ │ ├── auth.ts # Login/logout routes
│ │ └── tracking.ts # Click tracking API
│ ├── utils/
│ │ ├── auth.ts # Auth helpers
│ │ ├── db.ts # D1 data access layer
│ │ ├── cloudflare-access.ts # JWT decoding
│ │ ├── helpers.ts # Utility functions
│ │ └── map-data.ts # World map data fetching
│ └── views/
│ ├── layouts.tsx # Page layouts
│ └── leaflet-map.tsx # Interactive map component
├── public/
│ ├── track.js # Client-side tracking
│ ├── qr.js # QR code functionality
│ └── theme-customizer.js # Live preview for themes
├── migrations/
│ ├── 0001_initial_schema.sql # Database schema
│ └── 0002_seed_themes.sql # Default themes
├── truth-window/ # Development session logs
├── schema.json # Pipeline event schema
├── wrangler.jsonc # Worker configuration
├── AGENTS.md # AI agent guidelines
└── README.md # This file
Pipelines ingest high-volume event data without overwhelming downstream systems:
// Write event to stream
await env.EVENT_STREAM.send([{
timestamp: new Date().toISOString(),
event_type: 'click',
slug: 'my-talk',
out: 'https://clicked-link.com',
link_text: 'Read More',
city: 'San Francisco',
country: 'US'
}]);R2 Data Catalog uses Apache Iceberg for:
- Schema evolution - Add columns without breaking existing queries
- Time travel - Query historical data snapshots
- ACID transactions - Consistent reads and writes
- Efficient queries - Columnar format (Parquet) for fast aggregations
R2 SQL lets you query data in R2 directly:
SELECT
slug,
event_type,
COUNT(*) as event_count,
city,
country
FROM default.click_events_v6
WHERE event_type = 'click'
AND slug IN ('talk-1', 'talk-2')
GROUP BY slug, event_type, city, country
ORDER BY event_count DESCCloudflare Access provides seamless authentication:
// Middleware extracts JWT from Cf-Access-Jwt-Assertion header
const userInfo = getUserFromAccessJWT(jwtHeader);
// Auto-create user on first login
if (!await getUser(userInfo.email)) {
await createUser(userInfo.email, false);
}
// Set context for routes
c.set("userEmail", userInfo.email);
c.set("userName", userInfo.name);
c.set("isAdmin", user.is_admin);Junction table enables flexible permissions:
-- Add maintainer to link
INSERT INTO link_maintainers (link_slug, user_email, added_by)
VALUES ('my-talk', 'coworker@example.com', 'me@example.com');
-- Check if user can access link
SELECT COUNT(*) FROM link_maintainers
WHERE link_slug = 'my-talk' AND user_email = 'coworker@example.com';Traditional data pipelines require:
- Database servers to manage
- ETL jobs to maintain
- Message queues to configure
- Analytics databases to scale
- Authentication servers to secure
Cloudflare's approach:
- ✅ Serverless - No servers to manage
- ✅ Scalable - Handles millions of events
- ✅ Cost-effective - Pay only for what you use
- ✅ Simple - Standard SQL, familiar patterns
- ✅ Fast - Global edge network, low latency
- ✅ Secure - Built-in Zero Trust authentication
Events are written with a defined schema ensuring proper column structure:
timestamp(STRING) - Event timestampevent_type(STRING) - Type: page_view, click, qr_scanslug(STRING) - Outie slugurl(STRING) - Page URLout(STRING) - Clicked link destinationlink_text(STRING) - Anchor text clickedvisitor_id(STRING) - Anonymous visitor IDuser_agent(STRING) - Browser user agentreferer(STRING) - HTTP referercountry(STRING) - Country codecity(STRING) - City nameregion(STRING) - State/provincecolo(STRING) - Cloudflare data centerlatitude(STRING) - Geographic latitudelongitude(STRING) - Geographic longitudetimezone(STRING) - Timezone
⚠️ NoASaliases - userow['count(*)']instead ofrow.count⚠️ ORDER BYonly on partition keys - use__ingest_ts, nottimestamp⚠️ LimitedLIKEpatterns - no multiple%, must escape_as\_
- User visits
/dashboard(protected by Access) - Cloudflare Access intercepts, shows GitHub OAuth
- User authenticates with GitHub
- Access issues JWT, adds
Cf-Access-Jwt-Assertionheader - Worker middleware decodes JWT, extracts email
- Check D1 for user, create if first login
- Set context variables for routes
- Render dashboard
This project is perfect for teaching:
- Serverless Architecture - Building apps without servers
- Data Pipeline Design - Streaming, batching, querying
- Web Analytics - How tracking actually works
- Modern Data Stack - Apache Iceberg, Parquet, SQL on object storage
- Zero Trust Security - Authentication without passwords
- Collaborative Systems - Multi-user permissions
# Deploy Worker
npm run deploy
# Your app will be at:
https://your-worker-name.workers.dev- QUICKSTART.md - Quick setup guide
- AGENTS.md - AI agent development guidelines
- truth-window/ - Development session logs
09-cloudflare-access-github-auth.md- Latest session (GitHub auth migration)08-d1-migration-progress.md- D1 database migration- And more...
- Cloudflare Workers
- D1 Database
- Cloudflare Access
- Pipelines Documentation
- R2 Data Catalog
- R2 SQL
- Apache Iceberg
- Hono Framework
This is an educational demo project. Feel free to fork and modify for your own learning!
MIT - Feel free to use this for educational purposes!
Built with 🧡 using Cloudflare's serverless data stack
