···11-ORIGIN=https://pipes.yourdomain.com
22-PORT=3000
33-NODE_ENV=production
44-DATABASE_URL=data/pipes.db
11+# Pipes Secrets
22+# All other configuration is in config.yaml
33+# Copy this file to .env and fill in the secrets
5466-# Indiko OAuth Configuration
77-INDIKO_CLIENT_ID=ikc_xxxxxxxxxxxxxxxxxxxxx
88-INDIKO_CLIENT_SECRET=iks_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
99-INDIKO_ORIGIN=https://indiko.dunkirk.sh
55+# OAuth (Indiko)
66+INDIKO_CLIENT_SECRET=your_client_secret_here
77+88+# Session (generate with: openssl rand -base64 32)
99+SESSION_SECRET=your_random_secret_here
+17-6
.gitignore
···11-node_modules/
22-.wrangler/
33-dist/
44-.dev.vars
11+# Go
22+pipes
33+*.exe
44+*.dll
55+*.so
66+*.dylib
77+88+# Environment and config
59.env
66-bun.lockb
77-data/
1010+config.yaml
1111+1212+# Database
813*.db
914*.db-shm
1015*.db-wal
1616+data/
11171818+# OS
1219.DS_Store
2020+2121+# IDE
2222+.idea/
2323+.vscode/
+72
CLAUDE.md
···11+# Pipes - Project Instructions
22+33+This is a Go application following Herald's architecture patterns.
44+55+## Tech Stack
66+77+- **Language**: Go 1.24+
88+- **Database**: SQLite with direct SQL (no ORM)
99+- **Auth**: Indiko OAuth 2.0 server
1010+- **Logging**: charmbracelet/log for structured logging
1111+- **Frontend**: Go html/template + Vanilla JavaScript
1212+- **Deployment**: Single static binary
1313+1414+## Development Commands
1515+1616+```bash
1717+# Build the project
1818+go build -o pipes .
1919+2020+# Run in development
2121+./pipes serve -c config.yaml
2222+2323+# Run with hot reload (using a tool like air)
2424+air
2525+2626+# Initialize config files
2727+./pipes init
2828+2929+# Run tests
3030+go test ./...
3131+```
3232+3333+## Configuration
3434+3535+- **YAML Config** (config.yaml): All non-sensitive configuration
3636+- **Environment** (.env): Secrets only (INDIKO_CLIENT_SECRET, SESSION_SECRET)
3737+- YAML supports env var expansion: `${VAR}` syntax
3838+3939+See config.yaml.example and .env.example for templates.
4040+4141+## Architecture
4242+4343+Follow Herald's patterns:
4444+- Clean separation of concerns (config/, store/, auth/, engine/, nodes/, web/)
4545+- SQLite with WAL mode
4646+- Structured logging with charm log
4747+- Graceful shutdown with signal handling
4848+- Session-based authentication with Indiko OAuth
4949+5050+## Code Style
5151+5252+- Use `gofmt` for formatting
5353+- Structured logging: `logger.Info("message", "key", value)`
5454+- Error wrapping: `fmt.Errorf("context: %w", err)`
5555+- Context propagation for cancellation
5656+- Foreign key constraints in SQLite
5757+5858+## Design Aesthetic
5959+6060+**Neo-Brutalism** - Bold, geometric design:
6161+- Space Grotesk font
6262+- Hard borders (3-4px solid)
6363+- Hard box shadows (no blur)
6464+- High contrast colors
6565+- Sharp, geometric shapes
6666+- Color palette:
6767+ - Primary: #2563eb (blue)
6868+ - Secondary: #ff6b35 (orange)
6969+ - Auth/Indiko: #AB4967 (pink)
7070+ - Dark: #26242b (near-black)
7171+ - Background: #f5f5f0 (warm off-white)
7272+ - White: #fff
+187
CRUSH.md
···11+# Crush Memory - Pipes Project
22+33+## User Preferences
44+55+- Follow Herald's Go architecture patterns
66+- Use direct SQL for all database operations (no ORM)
77+- Follow neo-brutalist design aesthetic (blue/orange for app, pink only for auth)
88+- Use Indiko for authentication (OAuth 2.0 with PKCE)
99+- Run on port 3001 (Indiko runs on 3000)
1010+- Use charmbracelet/log for structured logging
1111+1212+## Architecture Patterns
1313+1414+### Authentication Flow
1515+- OAuth 2.0 client that uses Indiko for authentication
1616+- PKCE flow with code verifier/challenge (required by IndieAuth spec)
1717+- Auto-registration with Indiko (client ID = app URL)
1818+- Session-based auth with 30-day cookies
1919+- Automatic token refresh using refresh tokens
2020+2121+### Database (SQLite)
2222+- Direct SQL queries (no ORM like Kysely or GORM)
2323+- SQLite with WAL mode for concurrency
2424+- Foreign key constraints enabled
2525+- Schema created automatically on startup
2626+2727+### Project Structure
2828+```
2929+pipes/
3030+├── main.go # Entry point, CLI setup
3131+├── go.mod # Dependencies
3232+├── config/
3333+│ ├── app.go # Config struct & loading (like Herald)
3434+│ └── validate.go # Config validation
3535+├── store/
3636+│ ├── db.go # SQLite setup & schema
3737+│ ├── users.go # User operations
3838+│ ├── pipes.go # Pipe CRUD
3939+│ ├── executions.go # Execution history
4040+│ └── cache.go # Source cache operations
4141+├── auth/
4242+│ ├── oauth.go # OAuth 2.0 client (Indiko)
4343+│ ├── session.go # Session management
4444+│ └── middleware.go # Auth middleware
4545+├── engine/
4646+│ ├── executor.go # Pipeline execution engine
4747+│ ├── scheduler.go # Cron-based scheduler (Herald pattern)
4848+│ └── registry.go # Node type registry
4949+├── nodes/
5050+│ ├── node.go # Node interface
5151+│ ├── sources/
5252+│ │ ├── rss.go # RSS/Atom source
5353+│ │ └── http.go # HTTP API source
5454+│ └── transforms/
5555+│ ├── filter.go # Filter transform
5656+│ ├── sort.go # Sort transform
5757+│ ├── limit.go # Limit transform
5858+│ ├── merge.go # Merge sources
5959+│ ├── dedupe.go # Deduplicate items
6060+│ └── extract.go # Extract/transform fields
6161+└── web/
6262+ ├── server.go # HTTP server setup
6363+ ├── handlers.go # Route handlers
6464+ ├── api.go # JSON API endpoints
6565+ └── templates/
6666+ ├── layout.html # Base layout
6767+ ├── index.html # Landing page
6868+ ├── dashboard.html # User dashboard
6969+ ├── editor.html # Visual editor
7070+ └── style.css # Frutiger Aero styles
7171+```
7272+7373+### Database Schema
7474+- **users**: id, indiko_sub, username, name, email, photo, url, role, created_at, updated_at
7575+ - `indiko_sub` is the "sub" field from Indiko's userinfo endpoint
7676+ - `role` is either "user" or "admin" (synced from Indiko)
7777+- **sessions**: id, user_id, access_token, refresh_token, expires_at, created_at
7878+ - 30-day sessions with automatic token refresh
7979+- **pipes**: id, user_id, name, description, config (JSON), is_public, created_at, updated_at
8080+ - `config` is JSON: {version, nodes[], connections[], settings}
8181+- **scheduled_jobs**: id, pipe_id, cron_expression, next_run_at, last_run_at, enabled, created_at, updated_at
8282+- **pipe_executions**: id, pipe_id, status, trigger_type, started_at, completed_at, duration_ms, items_processed, error_message, metadata
8383+- **execution_logs**: id, execution_id, node_id, level, message, timestamp, metadata
8484+- **source_cache**: id, pipe_id, node_id, cache_key, data, etag, last_modified, expires_at, created_at
8585+8686+### OAuth Configuration
8787+- **Client ID**: App's URL (e.g., `http://localhost:3001`)
8888+- **Client Secret**: Optional, for pre-registered clients (stored in .env)
8989+- **Scopes**: `profile email`
9090+- **PKCE**: Required (S256 code challenge method)
9191+- **Callback URL**: `{ORIGIN}/auth/callback`
9292+9393+## Configuration
9494+9595+### YAML Config (config.yaml)
9696+Contains all non-sensitive configuration:
9797+```yaml
9898+# Server settings
9999+host: localhost
100100+port: 3001
101101+origin: http://localhost:3001
102102+env: development
103103+log_level: info # debug, info, warn, error, fatal
104104+105105+# Database
106106+db_path: pipes.db
107107+108108+# OAuth (Indiko)
109109+indiko_url: http://localhost:3000
110110+indiko_client_id: http://localhost:3001
111111+indiko_client_secret: ${INDIKO_CLIENT_SECRET} # Loaded from .env
112112+oauth_callback_url: http://localhost:3001/auth/callback
113113+114114+# Session
115115+session_secret: ${SESSION_SECRET} # Loaded from .env
116116+session_cookie_name: pipes_session
117117+```
118118+119119+### Environment Variables (.env)
120120+Contains **only secrets**:
121121+```env
122122+# OAuth (Indiko)
123123+INDIKO_CLIENT_SECRET=your_client_secret_here
124124+125125+# Session (generate with: openssl rand -base64 32)
126126+SESSION_SECRET=your_random_secret_here
127127+```
128128+129129+## Routes
130130+131131+### Public
132132+- `GET /` - Landing page (redirects to /dashboard if authenticated)
133133+- `GET /auth/login` - Start OAuth flow
134134+- `GET /auth/callback` - OAuth callback handler
135135+136136+### Authenticated
137137+- `GET /dashboard` - User dashboard (requires auth)
138138+- `GET /pipes/:id/edit` - Visual editor (requires auth)
139139+- `POST /auth/logout` - End session
140140+- `GET /api/me` - Get current user info
141141+- `GET /api/pipes` - List user's pipes
142142+- `GET /api/pipes/:id` - Get pipe config
143143+- `POST /api/pipes` - Create pipe
144144+- `PUT /api/pipes/:id` - Update pipe
145145+- `DELETE /api/pipes/:id` - Delete pipe
146146+- `POST /api/pipes/:id/execute` - Execute pipe manually
147147+- `GET /api/pipes/:id/executions` - Execution history
148148+- `GET /api/executions/:id/logs` - Execution logs
149149+- `GET /api/node-types` - Available node types
150150+151151+## Code Style
152152+153153+- Use `gofmt` for formatting
154154+- Structured logging with charmbracelet/log: `logger.Info("message", "key", value)`
155155+- Error wrapping: `fmt.Errorf("context: %w", err)`
156156+- Context propagation for cancellation
157157+- Session cookies named `pipes_session`
158158+- Authorization header: `Bearer {token}`
159159+160160+## Design Aesthetic
161161+162162+**Neo-Brutalism** - Bold, geometric design:
163163+- Space Grotesk font family
164164+- Hard borders (3-4px solid #26242b)
165165+- Hard box shadows (6-12px offset, no blur)
166166+- High contrast, sharp edges
167167+- Uppercase text, tight letter-spacing
168168+- Color palette:
169169+ - **App colors**:
170170+ - Primary: #2563eb (blue) - used for headings, secondary buttons
171171+ - Secondary: #ff6b35 (orange) - used for primary buttons, accents
172172+ - Background: #f5f5f0 (warm off-white)
173173+ - Dark: #26242b (near-black)
174174+ - White: #fff
175175+ - **Auth/Indiko colors** (login, error pages):
176176+ - Auth primary: #AB4967 (muted pink)
177177+ - Auth text: #fff (white - for text on pink buttons)
178178+179179+## Commands
180180+181181+```bash
182182+go build -o pipes . # Build
183183+./pipes serve # Run server
184184+./pipes init # Initialize config files
185185+./pipes help # Show help
186186+./pipes version # Show version
187187+```
+187-23
README.md
···11# Pipes
2233-This is my interperitation of yahoo pipes from back in the day! It is designed to allow you to string together pipelines of data and do cool stuff!
33+This is my interpretation of Yahoo Pipes from back in the day! It is designed to allow you to string together pipelines of data and do cool stuff with a modern Frutiger Aero aesthetic!
4455The canonical repo for this is hosted on tangled over at [`dunkirk.sh/pipes`](https://tangled.org/@dunkirk.sh/pipes)
6677+## Features
88+99+- 🔐 **Passwordless Authentication** - Uses Indiko for OAuth 2.0 authentication with passkeys
1010+- 🌊 **Visual Pipeline Builder** - Create data flows with an intuitive drag-and-drop interface
1111+- ⚡ **Scheduled Execution** - Pipes run automatically on cron schedules
1212+- 📊 **Data Sources** - RSS/Atom feeds and HTTP/REST APIs
1313+- 🔄 **Transform Operations** - Filter, sort, limit, merge, dedupe, and extract data
1414+- 🎨 **Neo-Brutalist Design** - Bold, geometric UI matching Indiko's aesthetic
1515+- 👥 **Role-based Access** - User and admin roles powered by Indiko
1616+1717+## Tech Stack
1818+1919+- **Language**: Go 1.24+
2020+- **Database**: SQLite with direct SQL
2121+- **Auth**: [Indiko](https://github.com/taciturnaxolotl/indiko) OAuth 2.0 server
2222+- **Frontend**: Go html/template + Vanilla JavaScript
2323+- **Deployment**: Single static binary
2424+725## Installation
8269271. Clone the repository:
···1331cd pipes
1432```
15331616-2. Install dependencies:
3434+2. Build the binary:
3535+3636+```bash
3737+go build -o pipes .
3838+```
3939+4040+3. Initialize configuration:
17411842```bash
1919-bun install
4343+./pipes init
2044```
21452222-3. Create a `.env` file:
4646+This creates a `config.yaml` file with sample configuration and a `.env.example` file for secrets.
4747+4848+Copy the example and add your secrets:
23492450```bash
2551cp .env.example .env
5252+# Edit .env with your actual secrets
2653```
27542828-Configure the following environment variables:
5555+Example `.env` file:
29563057```env
3131-ORIGIN=https://pipes.yourdomain.com
3232-PORT=3000
3333-NODE_ENV=production
3434-DATABASE_URL=data/pipes.db
5858+# Pipes Secrets
5959+# All other configuration is in config.yaml
6060+# Copy this file to .env and fill in the secrets
6161+6262+# OAuth (Indiko)
6363+INDIKO_CLIENT_SECRET=your_client_secret_here
35643636-# Indiko OAuth Configuration
3737-INDIKO_CLIENT_ID=ikc_xxxxxxxxxxxxxxxxxxxxx
3838-INDIKO_CLIENT_SECRET=iks_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3939-INDIKO_ORIGIN=https://indiko.dunkirk.sh
4040-INDIKO_REDIRECT_URI=https://pipes.yourdomain.com/auth/callback
6565+# Session (generate with: openssl rand -base64 32)
6666+SESSION_SECRET=your_random_secret_here
4167```
42684343-The database will be automatically created at `./data/pipes.db` on first run.
6969+The database will be automatically created at `./pipes.db` on first run.
447045714. Set up Indiko OAuth:
4646- - Go to your Indiko instance
4747- - Navigate to Admin → OAuth Clients
4848- - Create a new client with the redirect URI matching your `INDIKO_REDIRECT_URI`
4949- - Copy the Client ID and Secret to your `.env` file
7272+7373+Pipes uses auto-registration with Indiko, so you can start using it immediately! The client ID is just your app's URL (`http://localhost:3001`).
7474+7575+For production or to use role-based access control, ask your Indiko admin to pre-register your client with a client secret.
507651775. Start the server:
52785379```bash
5454-# Development (with hot reload)
5555-bun run dev
8080+./pipes serve -c config.yaml
8181+```
8282+8383+Or run without specifying a config file (uses environment variables from `.env`):
56845757-# Production
5858-bun run start
8585+```bash
8686+./pipes serve
5987```
60888989+Visit `http://localhost:3001` and sign in with your Indiko account!
9090+9191+## Configuration
9292+9393+Pipes uses a two-file configuration approach (just like Herald):
9494+9595+### YAML Config File (config.yaml)
9696+9797+Contains all non-sensitive configuration:
9898+9999+```bash
100100+./pipes init # Creates config.yaml and .env.example
101101+./pipes serve -c config.yaml
102102+```
103103+104104+Example `config.yaml`:
105105+```yaml
106106+# Server settings
107107+host: localhost
108108+port: 3001
109109+origin: http://localhost:3001
110110+env: development
111111+log_level: info # debug, info, warn, error, fatal
112112+113113+# Database
114114+db_path: pipes.db
115115+116116+# OAuth (Indiko)
117117+indiko_url: http://localhost:3000
118118+indiko_client_id: http://localhost:3001
119119+indiko_client_secret: ${INDIKO_CLIENT_SECRET} # Loaded from .env
120120+oauth_callback_url: http://localhost:3001/auth/callback
121121+122122+# Session
123123+session_secret: ${SESSION_SECRET} # Loaded from .env
124124+session_cookie_name: pipes_session
125125+```
126126+127127+### Environment Variables (.env)
128128+129129+Contains **only secrets** (never commit this file):
130130+131131+```env
132132+# OAuth (Indiko)
133133+INDIKO_CLIENT_SECRET=your_client_secret_here
134134+135135+# Session (generate with: openssl rand -base64 32)
136136+SESSION_SECRET=your_random_secret_here
137137+```
138138+139139+YAML config supports environment variable expansion using `${VAR}` syntax. Variables are loaded from `.env` file and can be overridden by system environment variables.
140140+141141+**Configuration precedence:** Environment variables > YAML config > defaults
142142+143143+### Log Levels
144144+145145+Set `LOG_LEVEL` (or `log_level` in YAML) to:
146146+- `debug` - Verbose output for troubleshooting
147147+- `info` - Standard operational messages (default)
148148+- `warn` - Warning messages
149149+- `error` - Error messages only
150150+- `fatal` - Fatal errors (exits immediately)
151151+152152+Example structured logging output:
153153+```
154154+2026/01/10 10:24:05 INFO starting pipes host=localhost port=3001 db_path=pipes.db
155155+2026/01/10 10:24:05 INFO user authenticated name="John Doe" email="john@example.com"
156156+```
157157+158158+## Architecture
159159+160160+Pipes follows Herald's clean architecture patterns:
161161+162162+```
163163+pipes/
164164+├── main.go # CLI entry point
165165+├── config/ # Configuration management
166166+├── store/ # Database operations
167167+├── auth/ # OAuth 2.0 client & session management
168168+├── engine/ # Pipeline executor & scheduler
169169+├── nodes/ # Node type definitions
170170+│ ├── sources/ # RSS, HTTP API sources
171171+│ └── transforms/ # Filter, sort, limit operations
172172+└── web/ # HTTP server & handlers
173173+ └── templates/ # HTML templates
174174+```
175175+176176+## OAuth Flow
177177+178178+1. User clicks "Sign in with Indiko"
179179+2. Redirect to Indiko authorization endpoint with PKCE
180180+3. User authenticates with passkey on Indiko
181181+4. User approves scopes (profile, email)
182182+5. Indiko redirects back with authorization code
183183+6. Exchange code for access + refresh tokens
184184+7. Create/update user in local database
185185+8. Create session with 30-day cookie
186186+187187+## Pipeline Execution
188188+189189+Pipelines are executed using topological sort (Kahn's algorithm):
190190+191191+1. Parse pipe configuration (nodes + connections)
192192+2. Build dependency graph
193193+3. Execute nodes in order, passing data between them
194194+4. Log execution progress
195195+5. Store results in database
196196+197197+The scheduler runs every minute, checking for pipes that need to execute based on their cron schedules.
198198+199199+## Available Node Types
200200+201201+**Sources:**
202202+- RSS Feed - Fetch items from RSS/Atom feeds
203203+- HTTP API - Fetch JSON data from REST APIs (coming soon)
204204+205205+**Transforms:**
206206+- Filter - Filter items based on field conditions
207207+- Sort - Sort items by field values
208208+- Limit - Limit the number of output items
209209+- Merge - Combine multiple data sources (coming soon)
210210+- Dedupe - Remove duplicate items (coming soon)
211211+- Extract - Transform/extract fields (coming soon)
212212+213213+## Development
214214+215215+Build and run:
216216+217217+```bash
218218+go build -o pipes .
219219+./pipes serve
220220+```
221221+222222+The database schema is automatically created on first run.
223223+61224<p align="center">
62225 <img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" />
63226</p>
···69232<p align="center">
70233 <a href="https://tangled.org/dunkirk.sh/indiko/blob/main/LICENSE.md"><img src="https://img.shields.io/static/v1.svg?style=for-the-badge&label=License&message=O'Saasy&logoColor=d9e0ee&colorA=363a4f&colorB=b7bdf8"/></a>
71234</p>
235235+
···11+package store
22+33+import (
44+ "database/sql"
55+ "fmt"
66+77+ _ "github.com/mattn/go-sqlite3"
88+)
99+1010+type DB struct {
1111+ *sql.DB
1212+}
1313+1414+func New(path string) (*DB, error) {
1515+ db, err := sql.Open("sqlite3", path)
1616+ if err != nil {
1717+ return nil, fmt.Errorf("open database: %w", err)
1818+ }
1919+2020+ // Enable foreign keys and WAL mode
2121+ if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
2222+ return nil, fmt.Errorf("enable foreign keys: %w", err)
2323+ }
2424+2525+ if _, err := db.Exec("PRAGMA journal_mode = WAL"); err != nil {
2626+ return nil, fmt.Errorf("enable WAL mode: %w", err)
2727+ }
2828+2929+ store := &DB{DB: db}
3030+3131+ // Initialize schema
3232+ if err := store.initSchema(); err != nil {
3333+ return nil, fmt.Errorf("init schema: %w", err)
3434+ }
3535+3636+ return store, nil
3737+}
3838+3939+func (db *DB) initSchema() error {
4040+ schema := `
4141+ -- Users (OAuth profiles)
4242+ CREATE TABLE IF NOT EXISTS users (
4343+ id TEXT PRIMARY KEY,
4444+ indiko_sub TEXT UNIQUE NOT NULL,
4545+ username TEXT,
4646+ name TEXT,
4747+ email TEXT,
4848+ photo TEXT,
4949+ url TEXT,
5050+ role TEXT NOT NULL DEFAULT 'user',
5151+ created_at INTEGER NOT NULL,
5252+ updated_at INTEGER NOT NULL
5353+ );
5454+5555+ -- Sessions (OAuth sessions)
5656+ CREATE TABLE IF NOT EXISTS sessions (
5757+ id TEXT PRIMARY KEY,
5858+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
5959+ access_token TEXT NOT NULL,
6060+ refresh_token TEXT,
6161+ expires_at INTEGER NOT NULL,
6262+ created_at INTEGER NOT NULL
6363+ );
6464+6565+ CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
6666+6767+ -- Pipes (pipeline configurations)
6868+ CREATE TABLE IF NOT EXISTS pipes (
6969+ id TEXT PRIMARY KEY,
7070+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
7171+ name TEXT NOT NULL,
7272+ description TEXT,
7373+ config TEXT NOT NULL,
7474+ is_public INTEGER NOT NULL DEFAULT 0,
7575+ created_at INTEGER NOT NULL,
7676+ updated_at INTEGER NOT NULL
7777+ );
7878+7979+ CREATE INDEX IF NOT EXISTS idx_pipes_user_id ON pipes(user_id);
8080+8181+ -- Scheduled jobs
8282+ CREATE TABLE IF NOT EXISTS scheduled_jobs (
8383+ id TEXT PRIMARY KEY,
8484+ pipe_id TEXT NOT NULL UNIQUE REFERENCES pipes(id) ON DELETE CASCADE,
8585+ cron_expression TEXT NOT NULL,
8686+ next_run_at INTEGER NOT NULL,
8787+ last_run_at INTEGER,
8888+ enabled INTEGER NOT NULL DEFAULT 1,
8989+ created_at INTEGER NOT NULL,
9090+ updated_at INTEGER NOT NULL
9191+ );
9292+9393+ CREATE INDEX IF NOT EXISTS idx_jobs_next_run ON scheduled_jobs(next_run_at, enabled);
9494+9595+ -- Execution history
9696+ CREATE TABLE IF NOT EXISTS pipe_executions (
9797+ id TEXT PRIMARY KEY,
9898+ pipe_id TEXT NOT NULL REFERENCES pipes(id) ON DELETE CASCADE,
9999+ status TEXT NOT NULL,
100100+ trigger_type TEXT NOT NULL,
101101+ started_at INTEGER NOT NULL,
102102+ completed_at INTEGER,
103103+ duration_ms INTEGER,
104104+ items_processed INTEGER,
105105+ error_message TEXT,
106106+ metadata TEXT
107107+ );
108108+109109+ CREATE INDEX IF NOT EXISTS idx_executions_pipe_id ON pipe_executions(pipe_id);
110110+ CREATE INDEX IF NOT EXISTS idx_executions_status ON pipe_executions(status);
111111+112112+ -- Execution logs (detailed step logs)
113113+ CREATE TABLE IF NOT EXISTS execution_logs (
114114+ id TEXT PRIMARY KEY,
115115+ execution_id TEXT NOT NULL REFERENCES pipe_executions(id) ON DELETE CASCADE,
116116+ node_id TEXT NOT NULL,
117117+ level TEXT NOT NULL,
118118+ message TEXT NOT NULL,
119119+ timestamp INTEGER NOT NULL,
120120+ metadata TEXT
121121+ );
122122+123123+ CREATE INDEX IF NOT EXISTS idx_logs_execution_id ON execution_logs(execution_id);
124124+125125+ -- Source cache (avoid redundant fetches)
126126+ CREATE TABLE IF NOT EXISTS source_cache (
127127+ id TEXT PRIMARY KEY,
128128+ pipe_id TEXT NOT NULL REFERENCES pipes(id) ON DELETE CASCADE,
129129+ node_id TEXT NOT NULL,
130130+ cache_key TEXT NOT NULL,
131131+ data TEXT NOT NULL,
132132+ etag TEXT,
133133+ last_modified TEXT,
134134+ expires_at INTEGER NOT NULL,
135135+ created_at INTEGER NOT NULL
136136+ );
137137+138138+ CREATE INDEX IF NOT EXISTS idx_cache_pipe_node ON source_cache(pipe_id, node_id);
139139+ CREATE INDEX IF NOT EXISTS idx_cache_expires ON source_cache(expires_at);
140140+ `
141141+142142+ _, err := db.Exec(schema)
143143+ if err != nil {
144144+ return fmt.Errorf("execute schema: %w", err)
145145+ }
146146+147147+ return nil
148148+}