Level 1
What Daedalus does
For product managers, designers, and anyone who wants the big picture - no engineering knowledge required.
🗄️
Experiment Archive
A searchable record of every concluded A/B test and feature flag run across Mobile.de's three product clusters. Find what was tested, who ran it, and what happened.
📝
Team Learnings
Authenticated team members can add written learnings and Figma design links to any experiment. The history is append-only - nothing gets overwritten or lost.
🤖
Slack Bot
When an experiment concludes, the bot automatically posts an announcement to the right Slack channel. Admins control which clusters are announced and customize the message template.
🔍
FAQ Index
When a support thread in Slack is marked resolved, the conversation is automatically indexed so teams can search for answers to questions that have already been answered.
🔒
Open to Read, Gated to Write
Anyone can browse experiments without logging in. Adding learnings, editing metadata, or managing the bot requires signing in with your Mobile.de Okta account.
What Daedalus is NOT: It is not a live experiment dashboard. It is not connected to real-time Kameleoon data during browsing. It is an archival record- a memory system for concluded experiments. A future module for live experiment monitoring and automated Q&A is planned; at this time Daedalus strictly does not provide it.
Level 2
How it's organized
Major components, data flow, and how the layers connect. Hover any component for its source file.
Ingestion pipeline (external)
Storage (databases)
Data layer (lib/data/)
Presentation (pages & routes)
External services
Level 3
Under the hood
Four concrete request traces through the system. Click to expand each flow.
Reference
Complete reference
All API routes, database tables, and environment variables.
API Routes
| Method | Path | Guard | Description | Source |
|---|---|---|---|---|
| GET | /api/experiments | Public | Search & filter experiments. Params: q, cluster, team, type, touchpoint, decision, date_from, date_to, page, per_page, sort | app/api/experiments/route.ts |
| GET | /api/experiments/:id | Public | Full experiment record (no results) | app/api/experiments/[id]/route.ts |
| GET | /api/experiments/:id/results | Public | Per-goal, per-variation statistical results | app/api/experiments/[id]/results/route.ts |
| PATCH | /api/experiments/:id | Auth | Write learning (appended), figma_links, touchpoint, decision | app/api/experiments/[id]/route.ts |
| GET | /api/admin/config | Auth | Read bot configuration | app/api/admin/config/route.ts |
| PATCH | /api/admin/config | Auth | Update bot config fields | app/api/admin/config/route.ts |
| GET | /api/admin/bot-events | Auth | Paginated bot activity log | app/api/admin/bot-events/route.ts |
| GET | /api/admin/channels | Auth | List Slack channels (max 500). Used by campaign admin UI for channel picker. | app/api/admin/channels/route.ts |
| GET | /api/admin/campaigns | Auth | List all enabled campaigns | app/api/admin/campaigns/route.ts |
| POST | /api/admin/campaigns | Auth | Create a new campaign | app/api/admin/campaigns/route.ts |
| PATCH | /api/admin/campaigns/:id | Auth | Update a campaign (name, template, recipient, channel, delay, condition, enabled) | app/api/admin/campaigns/[id]/route.ts |
| DELETE | /api/admin/campaigns/:id | Auth | Delete a campaign by id | app/api/admin/campaigns/[id]/route.ts |
| POST | /api/bot/tick | CRON_SECRET | Run one bot poll cycle. Called by external scheduler. | app/api/bot/tick/route.ts |
| POST | /api/bot/test-dm | CRON_SECRET | Dev/test: send a real Slack DM to { email } using the first enabled campaign with a fake experiment, bypassing BigQuery. | app/api/bot/test-dm/route.ts |
| POST | /api/slack/events | HMAC | Inbound Slack Event Subscriptions. Triggers FAQ indexing on resolved threads. Also accepts plain { channel_id, thread_ts } webhooks via x-cron-secret. | app/api/slack/events/route.ts |
| POST | /api/slack/actions | HMAC | Slack interactive component callbacks. Stub - returns 200, no logic yet. | app/api/slack/actions/route.ts |
| POST | /api/slack/commands | HMAC | Slack slash command handler. /faq <query> returns up to 3 ephemeral Block Kit results. | app/api/slack/commands/route.ts |
| GET | /api/faq/search | Auth | Search FAQ entries. Param: q (empty returns all). Returns { entries }. | app/api/faq/search/route.ts |
| POST | /api/faq/sweep | CRON_SECRET | Trigger thread registry sweep via after(). Returns { ok: true }. Run nightly to backfill thread_registry. | app/api/faq/sweep/route.ts |
| GET | /api/v1/widget/config | Public | Feedback widget init check. Returns { enabled: true, branding: false }. Guard: X-API-Key. | app/api/v1/widget/config/route.ts |
| GET | /api/v1/feedback | Public | Returns { feedback: [] }. Guard: X-API-Key. | app/api/v1/feedback/route.ts |
| POST | /api/v1/feedback | Public | Submit feedback. Multipart: data JSON + optional screenshot PNG. Creates GitHub issue; deduplicates by hash. Guard: X-API-Key. | app/api/v1/feedback/route.ts |
| PUT | /api/v1/feedback/:hash/screenshot | Public | Upload screenshot for a submitted feedback item. Patches GitHub issue body. Guard: X-API-Key. | app/api/v1/feedback/[hash]/screenshot/route.ts |
| GET | /api/kameleoon-explorer | Auth | Step-by-step Kameleoon API debug proxy | app/api/kameleoon-explorer/route.ts |
| GET | /api/kameleoon-monitor | Auth | Kameleoon data-quality audit | app/api/kameleoon-monitor/route.ts |
PostgreSQL Tables
| Table | Owner / purpose | Key columns | Source |
|---|---|---|---|
| experiment_user_data | User-generated fields. Written by patchExperiment. | experiment_id (PK), learning JSONB[], figma_links JSONB[], touchpoint, decision, updated_at | lib/db/schema.ts:59 |
| bot_config | Slack bot runtime configuration. One row expected. | id, announcement_channel_id, enabled | lib/db/schema.ts:22 |
| notified_experiments | Deduplication ledger. Prevents double-announcing experiments. | (experiment_id, trigger_type, campaign_id) composite PK, notified_at | lib/db/schema.ts:41 |
| bot_events | Audit log of every bot send attempt. | id, experiment_id, event_type, sent_at, destination, status (sent|failed|skipped) | lib/db/schema.ts:50 |
| faq_entries | Full-text searchable index of resolved Slack support threads. | id, experiment_id (unique), experiment_key, bq_experiment_id, question, answer, full_text, content_hash, search_vector (tsvector, GIN index), source, source_url, created_at, updated_at | lib/db/schema.ts:68 |
| campaigns | Campaign definitions driving the bot notification loop. | id, name, trigger_type, template JSONB, recipient_type, channel_id, delay_days, condition JSONB, enabled, created_at | lib/db/schema.ts:28 |
| thread_registry | Thread sweep registry. Tracks every support thread seen by the sweep job with resolution status. | id, thread_ts (unique), channel_id, permalink, reporter_user_id, category, experiment_key, opened_at, last_activity_at, resolved_at, resolution_signal, status (open|resolved|extracted|ignored), raw_thread_json, ignored_reason, extracted_at, created_at, updated_at | lib/db/schema.ts:86 |
| extractions | LLM-extracted Q&A entries from resolved threads. Pending human review before surfacing in FAQ. | id, thread_ts (FK), qa_type, question, scenario, answer, options JSONB, caveats JSONB, doc_links JSONB, platform JSONB, confidence, coe_note, status (pending_review|approved|rejected|superseded), review_channel_message_ts, reviewed_by, reviewed_at, prompt_version, model, search_vector, created_at, updated_at | lib/db/schema.ts:111 |
| provisioning_requests | Kameleoon account provisioning attempts. Written by handleProvisioningRequest when a provisioning message arrives in the configured Slack channel. | id, requester_email, requester_name, team, role, slack_message_ts (unique), kameleoon_status (created|duplicate|failed), kameleoon_account_id, error_message, created_at | lib/db/schema.ts:143 |
Environment Variables
| Variable | Required for | Notes |
|---|---|---|
| AUTH_SECRET | Write access | NextAuth v5 session encryption key |
| OKTA_CLIENT_ID | Write access | Okta application client ID. Must be a Web/confidential app, not SPA. |
| OKTA_ISSUER | Write access | Okta authorization server issuer URL |
| OKTA_CLIENT_SECRET | Write access | Used server-side only. Never reaches the browser. |
| DATABASE_URL | Persistence | Postgres connection string. Omit to use in-memory mock. |
| GOOGLE_CREDENTIALS_JSON | Real experiment data | Full GCP service account JSON, single-line string. Omit to use mock data. |
| BIGQUERY_PROJECT_ID | With GCP credentials | GCP project ID |
| BIGQUERY_DATASET | Optional | BigQuery dataset name. Defaults to daedalus. |
| REPO_GOLIVE_DATE | Optional | ISO date. Experiments after this date show learning CTA instead of silent empty state. |
| SLACK_BOT_TOKEN | Slack bot | Bot token (xoxb-...) for posting announcements |
| DAEDALUS_BASE_URL | Slack bot | Public base URL used in Slack message links |
| CRON_SECRET | Slack bot | Validated against x-cron-secret header on POST /api/bot/tick and POST /api/bot/test-dm |
| SLACK_SIGNING_SECRET | Slack events | HMAC-SHA256 signature verification for inbound Slack webhooks |
| NOTIFY_SINCE_DATE | Optional | ISO date. Bot skips experiments that ended before this date. |
| ANTHROPIC_API_KEY | FAQ extraction | Required by lib/extraction/client.ts for LLM-based thread extraction. Model defaults to claude-sonnet-4-6. |
| EXTRACTION_MODEL | Optional | Claude model ID override for FAQ extraction. Defaults to claude-sonnet-4-6. |
| FAQ_REVIEW_CHANNEL_ID | FAQ extraction | Private review channel ID for posting extraction review messages (e.g. C0BJ453E0GN = #daedelus-faq-review). Bot must be a member. |
| FEEDBACK_API_KEY | Feedback widget | API key for all /api/v1/* routes. Sent as X-API-Key header. |
| NEXT_PUBLIC_FEEDBACK_PROJECT_ID | Feedback widget | Browser-side project ID used as the X-API-Key value by the floating widget. |
| FEEDBACK_ALLOWED_DOMAIN | Feedback widget | Email domain allowed to see the floating feedback widget (e.g. adevinta.com). |
| GITHUB_TOKEN | Feedback widget | GitHub personal access token for creating issues in the feedback repo. |
| GITHUB_OWNER | Feedback widget | GitHub repo owner (org or user) for feedback issues. |
| GITHUB_REPO | Feedback widget | GitHub repo name for feedback issues. |
| GITHUB_API_URL | Optional | GitHub API base URL. Defaults to api.github.com. Override for GitHub Enterprise. |
| KAMELEOON_CLIENT_ID | Internal tools | Required for /monitoring and /kameleoon-explorer only |
| KAMELEOON_CLIENT_SECRET | Internal tools | Kameleoon API client secret |
| KAMELEOON_SITE_CODE | Internal tools | Kameleoon site code |
Data Ownership Split
The two data stores have a clear ownership boundary. BigQuery is read-only from this codebase - writes only happen via the external Kameleoon pipeline. Postgres is fully owned by this app.
| Store | Written by | Fields |
|---|---|---|
| BigQuery | External Kameleoon pipeline | name, description, status, goals, variations, created_by_email, date_started, date_ended, runtime_days, team, cluster, site_code, environment_key, ingested_at |
Postgres experiment_user_data | This app - patchExperiment | learning (append-only JSONB[]), figma_links, touchpoint, decision |
| Postgres (bot tables) | This app - bot poll cycle | bot_config, campaigns, notified_experiments, bot_events |
Postgres faq_entries | This app - Slack event webhook + sweep cron | full_text, search_vector (tsvector), content_hash, experiment_key, bq_experiment_id, question, answer, source_url |
Postgres thread_registry, extractions | This app - sweep cron + LLM extraction pipeline | thread_registry: thread lifecycle + resolution status. extractions: LLM-extracted Q&A pending review. |
Scale conventions (Kameleoon API verbatim - do not normalize):
improvementRate is percentage scale (4.2 = +4.2%). reliability is 0-100 scale (97.0 = 97%). conversion_rate is fraction scale (0.042 = 4.2%). These are stored raw; the frontend transforms for display.