Ahead of Devcon 8 (Mumbai/India), we need a clear picture of everything AV-related in
efdevcon/monorepo: livestreams, video delivery, graphic generation, and the data
pipelines that feed the archive. The repo contains a handover document
(docs/now.md, AV
section pointing to
pretalx-pipeline.md
alongside this doc) which states the AV enrichment pipeline was never built because
the AV team wasn't formed at the time. This document maps what actually exists,
separates live from dormant, and lists what will break for DC8 if untouched.
For what happened during Devcon SEA's event week itself (livestream player built the day
before, the monitor page, pipeline write-backs that erased YouTube ids and Swarm hashes,
schedule publish cadence), see
devcon-sea-event-week-retro.md.
Sections 1-9 are assessment only; §10 suggests sequencing and §11 records the architecture recommendations from the review discussion. Decisions belong with the AV team.
Assessment date: 2026-08-04. Items marked ✅ FIXED were implemented the same day - §12 is the changelog. On 2026-08-05 the §2c app decision landed (event-app) and its AV surface was built - §12c is that changelog. On 2026-08-10 compression + CDN cache headers landed on devcon-api (the §11.7 insurance) - §12e is that changelog. Reviewed 2026-08-10: findings re-verified against the repo and live API, GitHub links added, superseded statements corrected in place. Updated 2026-10-09, after the first Devcon 8 schedule release: §12g covers the Pretalx upgrade, the partner schedule sync, the release guard and the featured-speaker question. Line references may drift as code changes.
There is no database and no media infrastructure of Devcon's own.
- Hosting: the API (
api.devcon.org) runs on Render. Committing todevcon-api/data/redeploys it (Render boots the new instance in parallel, so no downtime), which is how git-committed changes reach the running API. - Data layer:
devcon-apihas no Prisma/SQLite/Postgres. Synced data lives as plain JSON committed intodevcon-api/data/, loaded into memory at boot bydevcon-api/src/data/store.ts. Between deploys the data is also updated on the fly: the Pretalx webhook swaps the in-memory store in real time (hooks.ts→store.ts), andPUT /sessions/sources/:idwrites to memory immediately - the git commit is for the next boot (eventual consistency - AV commits are even tagged[skip deploy],sessions.ts, so they don't force a restart). Writes go back to git viadevcon-api/src/services/github.ts(CommitSession). Git is the system of record. - Playback: no player library exists anywhere (no react-player/video.js/hls.js/plyr/
vidstack - verified against every
package.json). No HLS, RTMP, DASH, WebRTC, Mux, or Cloudflare Stream. Everything is third-party iframes (YouTube, StreamEth, Etherna/Swarm, stm.live) plus one raw<video>in the archive's IPFS tab. - Ingestion: Pretalx (
cfp.devcon.org) is the source of truth for schedule; YouTube is the system of record for video. AV enrichment happens viaPUT /sessions/sources/:id, which updates memory and commits to git. - Consumers of the API: devcon-app (DC7) and
event-app (the DC8 PWA -
sessions/speakers/livestreams), the archive
(
archive/), devcon.org (/dips), devcon-ai (RAG embeddings built during sync), and Meerkat (session Q&A - pulls the schedule from Pretalx itself, the sync script pings it on every run so it refreshes; §3 #11).
Consequence: "building the AV pipeline" is entirely a data problem. No encoding, storage, or player work is implied.
High level (matches the internal architecture diagram, plus the consumers):
flowchart TD
PX[Pretalx<br/>schedule published] -->|webhook| API[devcon-api]
API -->|update in real time| STORE[in-memory store]
API -->|trigger| GHA[GitHub Action]
GHA --> ROS[generate run of show]
ROS -.-> AV[AV]
AV -->|enrich sessions with stream/video URL<br/>via git commit| DATA
GHA -->|commit Pretalx data| DATA[(devcon-api/data<br/>- devcon monorepo)]
DATA -.triggers api restart and is the source of truth at boot-up,<br/>giving us eventual consistency.-> API
API -->|fetch sessions / speakers| APP[Devcon App / event-app]
GHA -->|sync ping| MK[Meerkat]
MK -.->|pulls schedule| PX
In detail:
Pretalx (cfp.devcon.org, schedule published)
└─webhook──> POST /hooks/pretalx/:eventId/schedule (devcon-api/src/controllers/hooks.ts)
├─ in-memory atomic swap (store.ts, zero downtime)
└─ TriggerWorkflow() per WORKFLOW_MAP (hooks.ts)
├─ sync-pretalx-<event>.yml ──> commits devcon-api/data/ ──> API restart
│ ├─> devcon-ai `sync:sessions` (RAG embeddings)
│ └─> POST app.meerkat.events sync ping (test-devcon-8, devcon8; §3 #11)
└─ run-of-show-<event>.yml ──> upserts the AV team's run-of-show sheets (§2e)
AV / encoding vendor
└─PUT /sessions/sources/:id (API key) ──> memory + CommitSession() to git
fields: sources_{youtubeId,ipfsHash,swarmHash,
livepeerId,streamethId}, transcript_vtt,
transcript_text, duration
Details that matter:
WORKFLOW_MAPinhooks.tswires devcon8's and test-devcon-8's run-of-show to regenerate on every schedule publish; devcon-7's does not (sync only).- The webhook also accepts slug-less
POST /hooks/pretalx/scheduleand resolves the event from the Pretalx slug in the payload. ✅ FIXED 2026-08 (§12): it used to fall back todevcon-7for an unknown slug, silently resyncing the wrong event; unknown slugs now get a 400 + warning log. - The webhook acks 204 immediately and syncs detached (✅ 2026-08-19, §12f): the plugin sends the POST from inside Pretalx's release request, so holding the response for the sync stalled — and rolled back — orga-UI releases.
- Both sync workflows also have a monthly cron fallback (
0 23 30 * *), so data refreshes even if no schedule is published. - Releasing a Devcon 8 schedule is done by the speaker team in the Pretalx orga UI,
nowhere else.
pnpm pretalx:release(devcon-api, §12f) refusesdevcon8through a hardcodedNEVER_RELEASElist with no override (2026-10-08), so a script run cannot publish by accident. The first Devcon 8 schedule was released on 2026-10-09: the webhook dispatched the sync, which committed 316 sessions, 320 speakers and 11 rooms; the run-of-show workflow dispatched alongside it failed on the Google Sheets write quota, fixed the same day (§12g). - The devcon-7 sync additionally runs
createPresentations()(Google Slides) and a glossary build - gatedif (eventId === 'devcon-7')insync-pretalx.ts. The slides pipeline end to end is in §2d. - Speaker emails never leave the sync: each speaker gets
hash = HMAC-SHA256(EMAIL_SECRET, email)(pretalx.ts) so downstream tools can match a signed-in email against confirmed speakers without the API exposing addresses. Two implications: rotatingEMAIL_SECRETchanges every speaker hash, and if the env var is missing the sync just warns and skips hashing - no hard failure. No in-repo consumer today (the visa speaker form queries Pretalx directly), but treat the secret as stable. - AV write auth:
PUT /sessions/:idandPUT /sessions/sources/:idcheck an API key againstSERVER_CONFIG.API_KEYS(src/middleware/apikey.ts). ✅ IMPROVED 2026-08 (§12): the key is now accepted via thex-api-keyheader (preferred - query-string keys land in Render/proxy access logs);?apiKey=still works for existing tooling. Issue DC8 vendor keys with the header form. - Pretalx instance: everything lives on
https://cfp.devcon.org(migrated frommum.speakat.xyzin Aug 2026;speak.devcon.orgretired before that). AllPRETALX_BASE_URIentries indevcon-api/src/utils/config.tsnow point there. Gotcha: a stale base URL is worse than a broken one - fetch strips theAuthorizationheader on the cross-origin 301 redirect, so the sync runs anonymously and private events 401 while public ones keep working. The secret namePRETALX_API_KEY_MUMBAIpredates the rename and is kept as-is.
Three write paths. Both endpoints authenticate with an API key (x-api-key header
preferred, ?apiKey= query still accepted) and commit to git with [skip deploy].
Path A - PUT /sessions/sources/:id (the day-of enrichment endpoint,
sessions.ts).
✅ FIXED 2026-08 (§12): now patch semantics - omitted fields keep their current
value, an explicit '' clears (it used to be a full replace where a partial payload
wiped everything unsent). Bumps the version of the session's own event (the devcon-7
hardcode of blocker #1 is gone).
| Field | Consumed by |
|---|---|
sources_youtubeId |
YouTube embed in devcon-app + archive; yt.ts thumbnail/description push; coverage stats |
sources_streamethId |
StreamEth iframe tab |
sources_swarmHash |
Swarm player (devcon-app only; also gates the player-switcher pills, §7.3) |
sources_ipfsHash |
Archive IPFS tab (dead gateway, §6) - vestigial since DC6 |
sources_livepeerId |
Nothing (1 session ever had one) |
transcript_vtt |
Nothing - never parsed anywhere (§8) |
transcript_text |
LLM/AI consumption only |
duration |
Duration display in app + archive |
Path B - PUT /sessions/:id (generic session update). True partial merge - only
sent keys change, unknown keys are rejected. Can update any existing session field,
notably resources_slides, title/description corrections, slot_* moves.
✅ FIXED 2026-08 (§12): it now bumps the event version too (it used not to, so
corrections could serve stale to clients and OG cards indefinitely).
Path C - hand-edited git commit (no endpoint). Per-room livestream config in
data/rooms/<event>/*.json:
youtubeStreamUrl_1..4 (one per event day) and translationUrl. Survives Pretalx
syncs only via spread order (§5.3).
One table for everything that runs (or is supposed to run) without a human editing JSON by hand. "Manual" means someone runs a pnpm script locally.
| Automation | What it does | Trigger | Event scoping | Status |
|---|---|---|---|---|
Pretalx webhook (hooks.ts) |
Full resync into memory + dispatches workflows | Pretalx "schedule published" plugin webhook | Slug-resolved; unknown slugs rejected with 400 (✅ fixed, §12) | Live |
sync-pretalx{,-devcon8,-test-devcon-8}.yml |
Commits devcon-api/data/ to git (→ Render redeploy), builds devcon-ai RAG embeddings, pings Meerkat |
Webhook-dispatched + monthly cron (0 23 30 * *) |
devcon-7 / devcon8 / test-devcon-8 | Live |
run-of-show-{devcon8,test-devcon-8}.yml |
Upserts the AV team's run-of-show sheets, one per day and stage (§2e) | Webhook-dispatched + manual dispatch | devcon8, into the AV Drive next to the intro cards (since 2026-10-20); the test-devcon-8 job is switched off (if: false, run it locally); devcon-7 runs locally from committed data as the preview |
Live |
pnpm sync:eventyay <event> (sync-eventyay.ts) |
Syncs a partner's eventyay schedule (Open Tech Summit) into our Pretalx event: proposals, WIP slots, tags, speakers (§12g) | Manual; dry run by default, --apply writes |
test-devcon-8 / devcon8 (target table in the script) | Manual |
pnpm pretalx:release <event> (pretalx-release.ts) |
Releases a schedule version through the Pretalx API (orga-UI fallback, §12f) | Manual | devcon8 refused (NEVER_RELEASE); test-devcon-8 and devcon7-sea |
Manual |
Meerkat ping (notifyClients() in sync-pretalx.ts) |
POST sync ping to app.meerkat.events, which then pulls the schedule from Pretalx |
Runs inside the sync script | test-devcon-8 / devcon8 (MEERKAT_EVENTS map; ✅ 2026-10-09, §12g) |
Live |
pnpm yt → syncThumbnails() (yt.ts) |
Renders devcon.org/api/social/av/:id at 1920×1080 and pushes via youtube.thumbnails.set() (105/run for quota) |
Manual, interactive browser OAuth | sessions/devcon-7 hardcoded |
Manual |
pnpm yt → syncDescriptions() |
Rewrites YouTube titles (truncated to fit "by " + a hardcoded | Devcon SEA suffix into 100 chars) and descriptions (from session description + tags), ledger youtube-descriptions.json |
Commented out in main() |
devcon-7 + SEA branding hardcoded | Disabled |
pnpm import:yt (import-yt.ts) |
Imports YouTube playlists (data/playlists.json) into session JSONs - how devconnect-arg's 418 sessions got in |
Manual, Google service account | eventId = 'devconnect-arg' hardcoded |
Manual |
pnpm stats:v (stats-video.ts) |
AV source-coverage report | Manual | ✅ Takes an event id since 2026-08 (§12) | Manual |
| devcon.org social/OG routes | On-demand edge-rendered cards with a Supabase render cache (see §4) | HTTP, devcon.org/api/social/* |
DC7 placeholder art in dc8/ (asset swap pending, #6) |
Live (§12d) |
generate-images.yml (nested .github) |
DC6 lower-thirds + social cards | Hourly cron in a nested .github GitHub never executes |
DC6 | Dead (§8) |
pnpm slides (slides.ts) |
Google Slides PDF export + Drive migration (§2d) | Manual | data/slides/ gone → throws |
Dead (§9) |
✅ DECIDED 2026-08-05: event-app carries DC8's AV surface (see §12c for what was
built). The handover doc positions
event-app as the new PWA
going forward (offline-first: Dexie + SWR, Serwist service worker, optional native
iOS/Android via Capacitor - see
event-app/docs/architecture.md).
devcon-app stays DC7-only; its blocker #3 is moot.
- event-app's AV surface now exists (it originally had none - the zod models
stripped the AV fields at validation). Today:
SessionSchemacarriessources_*,RoomSchemacarriesyoutubeStreamUrl_1..4/translationUrl(event-app/src/data/models/), the provider maps them (normalising the API's''toundefined), andSessionMedia.tsxembeds recordings (YouTube, else StreamEth) with a live-stream fallback selected by event-relative day indexing (eventDayIndex/streamUrlForDayincomponents/schedule/utils.ts), plus a "Live translation available" link while a session is live/imminent and its room has atranslationUrl. - Data source: the active provider is
DevconApiProvider→ devcon-api, with a runtime-switchable dataset (?dataset) offeringtest-devcon-8,devcon8anddevcon-7(event-app/src/data/dataset.ts). A second, currently inactive provider (devcon.provider.ts) reads Pretalx directly through/api/pretalx- a server-side proxy hardcoded toEVENT_SLUG = 'test-devcon-8'(route.ts); flip that slug if the provider is ever reactivated. - Room screens (venue signage) live here now:
/room-screens/[id]renders a per-room now/next display, relaid out 2026-09-17 for TVs and laptops (sized in a 16:9 design unit; branded header with the stage crest and clock, live progress bar, next-up rail). Its "Scan" row carries up to three QR codes: "Open in app" (the session), "Watch livestream" for the room's stream on the current conference day (anchored on now via the mockable clock, unlike SessionMedia which anchors on the session's own day - intentional; hidden when no stream URL exists), and "See questions" to Meerkat's presenter view for the room's stage, keyed by our room id since 2026-10-09 (only when Meerkat lists sessions for that stage, seedocs/meerkat.md)./room-screensis the picker, one card per room with its live/next session. devcon-app has the DC7 predecessor atdevcon-app/src/pages/room-screens/[id].tsx(text-only, no QR). - Meerkat's user-facing half is in event-app: the session pages show the
questions (public API, SSE stream) and "Ask a question" links to
GET /api/meerkat/go, which gates on the Supabase auth cookies + a paid Pretix ticket and 302s to Meerkat with a 5-min HS256 JWT (secret shared with Meerkat). For stage screens and streams, Meerkat's presenter view (/stage/<stage>) follows the live session; options and prerequisites indocs/meerkat.md. The schedule-sync half is §3 #11. - event-app's
/api/admin/{datasets,search,inference}routes are the debugging UI for the devcon-ai RAG stack (per the handover doc). - devcon-app meanwhile shows a dismissable "Devcon 8 prep" banner but is otherwise still fully DC7-wired.
How every DC7 speaker got a pre-made deck and how the archive got its PDFs. Built by Wesley between Oct and Dec 2024; everything lives in devcon-api plus one devcon.org page. Until 2026-09-28 nothing was written back to Pretalx, it only supplied titles, codes and speaker emails; since then the sync writes each deck's URL into a Pretalx question (step 2).
- Deck creation -
clients/slides.tsCreatePresentationFromTemplate(title, pretalxCode, speakerEmails): the EF Google service account copies one template deck into a shared-Drive folder, names it<title> [<PRETALX_CODE>]and grants each speaker emailwriteraccess with the Drive notification email off (sendEmails = false). Aname contains '[code]'lookup makes it idempotent. The first bulk run was manual (pnpm slides, 2024-10-11). The shared drive and the template deck are common to all events (SLIDES_DRIVE_ID,SLIDES_TEMPLATE_ID); the folder that receives the decks is per event, named after it (SLIDES_FOLDER_ID_TEST_DEVCON_8,SLIDES_FOLDER_ID_DEVCON8) and mapped per instance inutils/config.ts(2026-09-30), so test decks and Devcon 8 decks cannot share a folder. No defaults: unset means the run fails before touching Drive (2026-09-17, the DC7 ids were removed from the code); keep the ids out of docs. The DC7 template deck itself is shared "anyone with the link" (checked 2026-09-17); copies do not inherit that, but the master should be restricted so nobody outside can edit what every deck is copied from. - Wired into the Pretalx sync -
sync-pretalx.tscreatePresentations()(2024-10-28): for everydata/sessions/devcon-7/*.jsonwithoutresources_presentationit creates the deck and writeshttps://docs.google.com/presentation/d/<id>into the JSON.runPermissions()(2024-11-09) re-grants writer access to the current speaker emails on existing decks, which covers speakers added later. Both run inside the Pretalx Sync GitHub Action with the Google credentials as secrets, so during DC7 every schedule publish also created decks for new talks. Since 2026-09-17 two gates replace thedevcon-7check: a hardcodedSLIDES_ALLOWED_EVENTSlist in the sync (test-devcon-8and, since 2026-09-30,devcon8; devcon-7 is over and stays out) and theSLIDES_EVENTSenv opt-in for the run. CI sets none of theSLIDES_*variables, so no workflow creates decks; Devcon 8 decks are created by manual runs for now, the first one on 2026-09-30 for a single accepted talk (SLIDES_EVENTS=devcon8 SLIDES_ONLY_CODES=<code> pnpm sync:pretalx:devcon8), then on 2026-10-08 for the 43 Open Tech Summit sessions (§12g).SLIDES_ONLY_CODESlimits a run to a few Pretalx codes andSLIDES_SKIP_PERMISSIONS=truecreates decks without granting speakers, both meant for test runs.pnpm slides(§9) now exits immediately. A permissions pass (2026-09-18,reconcilePermissions()in the sync, rules inutils/slides-permissions.ts) runs after deck creation for the same events: current speakers get writer, the AV group fromSLIDES_AV_GROUPgets reader,anyonegrants are removed unlessSLIDES_ALLOW_PUBLIC=true. A person's access is never revoked (since 2026-10-16): Drive does not record who granted a permission, so a speaker removed in Pretalx and a co-author shared by hand look the same. Writers who are not speakers are listed in the log ("left as is") for the team to check; addresses inSLIDES_KEEP_EMAILSare left out of that list, and inherited access is never touched. It only reports whileSLIDES_SKIP_PERMISSIONS=trueorSLIDES_PERMISSIONS_DRY_RUN=true. It reads decks by the id in the session JSON, so it needs the Google identity to see the decks: as of 2026-09-18 the external service account loses sight of files it creates seconds after creation (cause under investigation with the Workspace admins), which blocks this pass and the export.GOOGLE_IMPERSONATE_USER(domain-wide delegation, to be authorised by an admin) makes the pipeline act as an internal EF user instead. Partner tracks (2026-10-08): a deck whose session sits on a track listed in the instance'sSLIDES_DELEGATE_TRACKS([CLS] - OTSon both DC8 events) is shared with the partner's contact address fromSLIDES_PLACEHOLDER_DELEGATE_<EVENT>only, at creation and in the permissions pass; the partner passes it on to its speakers. Outside those tracks a placeholder speaker address (§12g) also maps to that contact, real addresses are granted as usual. Deck link and no-Google-account speakers recorded in Pretalx (2026-09-28): the slides passes now run over every accepted or confirmed submission (accepted since 2026-09-29: the acceptance email may carry the link before the speaker confirms), scheduled or not, and write each deck URL into a submission question ("Slides deck", url, private, frozen so speakers see it read-only; id inPRETALX_QUESTIONS_SLIDES_DECK). Pretalx is the source of truth: the session mapper copies the answer intoresources_presentationonce the talk is published, and a URL that only exists on disk is backfilled into Pretalx. Drive refuses a silent grant to an address without a Google account and only accepts one with its invitation email (not delivered in testing, 2026-09-18); such addresses are recorded in a second question ("Slides: no Google account", text, private, frozen; id inPRETALX_QUESTIONS_SLIDES_NO_GOOGLE_ACCOUNT), shown read-only on the proposal page to organisers and to the speakers themselves, with a help text explaining the Drive invitation, and listed on the question's organiser page and in the API for the speaker team to share the deck by hand or to export to the run of show. Pretalx has no organiser-only mode for a question (an inactive one is hidden from the proposal page for everyone,limit_teamsonly scopes organiser teams), the deployed version ignores the per-question list filters, and its API rejected every tag update on a proposal (fixed by the 2026-10-08 upgrade, §12g), so speaker-visible read-only was the workable option (decided 2026-09-28). Addresses leave the list when they leave the talk or an organiser clears the field; Drive gives no signal for an accepted invitation. Non-public tags are now dropped from the session JSON by the session mapper (they used to be published like any other tag). Deck activity (2026-09-29): a third question, "Slides: last edit", holdsuntouchedwhile Drive's last modifier of the deck is still the pipeline's own identity (the impersonated user, else the service account) andedited <date>afterwards, the DC7 event-eve nudge's signal made persistent for the speaker team and the run-of-show export. Onefiles.getper deck per run, written to Pretalx only when the value changes, as fresh as the last sync; opening without editing counts as untouched, any content change by anyone counts as edited. Like the no-Google-account list it is mapped only on contact reads and never reaches the session JSON. Stale decks (2026-09-29): a talk that is rejected, withdrawn or otherwise no longer accepted/confirmed keeps its deck and the speakers' writer access, since the pipeline never deletes files or revokes access on its own. The sync lists such decks in the final problems list so the team can archive the deck or revoke access by hand. Field definitions, identical ontest-devcon-8(ids 178/179/180) anddevcon8(ids 181/182/183, created 2026-09-30 via the API; all: target submission, optional, not public, not visible to reviewers, active,freeze_afterin the past):- "Slides deck", variant url. Help text: "[Read-only, set by the Devcon team] Your Google Slides deck for this session, shared with each speaker's email address. Build your presentation in it. If you cannot open or edit it, email speak@devcon.org and provide a Google account address." (plain text: the url variant's help text is not rendered as markdown on the deployed pretalx, the text variant's is.)
- "Slides: no Google account", variant text, contains personal data. Help text: "[Read-only, set by the Devcon team] This email address has no Google account, so the deck could only be shared by invitation email. If you cannot edit the slides, contact speak@devcon.org and provide a Google account address."
- "Slides: last edit", variant text. Help text: "[Read-only, set by the Devcon team]
Untouched until someone edits the deck, then the date of the last edit, from the deck's
Google Drive history."
The mail template links
https://devcon.org/presentation/devcon8/{proposal_code}/; pretalx templates have no placeholder for custom-question answers. This replaced the sync's own "we need a Google account" email (2026-09-18 to 09-28). Both writes need the event's write token,PRETALX_API_KEY_WRITE_TEST_DEVCON_8orPRETALX_API_KEY_WRITE_DEVCON8(one organiser token per event, each scoped to that event's team, mapped per instance inutils/config.tssince 2026-09-30); without it the slides passes do not run.pretalx:releaseuses the same mapping.
- Speaker-facing link -
devcon/src/pages/presentation/[event]/[code].tsx(2026-09-28; the 2024/sea/presentation/<code>page stays as is, so DC7 links keep working):devcon.org/presentation/{devcon8|sea|test}/<code>/reads the submission's "Slides deck" answer live from Pretalx, server-side with a one-minute CDN cache, and redirects to it, so the link works before the schedule is published; only Google Docs/Drive URLs are followed and only accepted/confirmed talks resolve in production. Devcon SEA has no such question and falls back toapi.devcon.org/sessions/<code>. Without a link the page shows "No presentation link found. Please contact the organisers." Speakers get the link from Pretalx mail templates. - Event-eve nudge -
RunPermissions(commit553ca239a, 2024-11-12 00:59 Bangkok) reads the deck'slastModifyingUser. If the last editor is still the service account, meaning the speaker never opened the deck, it re-grants the speakers writer access, silently: despite its "Sending Notification Email" log line the grant hassendNotificationEmail: false, and the Drive-notification branch in the creation path was gated by asendEmailsflag that wasfalsein every commit (checked 2026-09-18). Drive never emailed a DC7 speaker; the deck link travelled in Devcon's own emails (step 3), and a grant to an address without a Google account failed with "Grant manually" in the log. - Post-event export to the archive -
scripts/slides.ts(2024-12-09 to 12):exportSlides()pulled each deck as PDF through the Slides export URL intodata/slides/devcon-7/<id>.pdfand setresources_slidesto the API URL;cleanupSlides()deleted PDFs that were empty or byte-for-byte the untouched template (size heuristic);migrateSlides()uploaded the remaining PDFs to Drive, rewroteresources_slidestohttps://drive.google.com/file/d/<id>/viewand removed the local files. That is whydata/slides/no longer exists and the script is dead today (§9). The archive still cannot show these links because of theslidesUrlfield mismatch (§7.1).
When slides were visible during DC7 (from git; the slides client never held an
anyone grant at any point in its history, so read access was never changed by code):
| Date | What became visible, and to whom |
|---|---|
| 2024-10-15 | Redirect route devcon.org/sea/presentation/<code> live, used by speakers |
| 2024-10-28 | Deck URLs committed to the public repo and served by the public API for all sessions; the decks themselves stayed restricted to speaker emails plus shared-drive members |
| 12 to 15 Nov 2024 | No change. Neither devcon-app nor the archive displayed deck or slide links to attendees |
| 2024-12-09 to 12 | PDF exports published as resources_slides, first API-hosted, then Drive links |
| Since then | Still not rendered in the archive because of the slidesUrl mismatch (§7.1) |
A stranger following the public URL hit Google's request-access screen before, during and after the talk. The only way a deck could be read early was a speaker changing its sharing themselves, which git cannot show. Slide content reached the public only in December, about four weeks after the event.
For DC8 the recommendation is to keep this pipeline, keep decks private until after the
event as in 2024, and enforce that instead of leaving it to each deck's sharing setting:
speakers write, the AV team reads from creation, the public reads only after the event
(§11.9). Reuse checklist (allow-list, questions and manual runs done 2026-09-30; CI still
pending): add SLIDES_EVENTS=devcon8 to the devcon8 sync workflow, with the Google secrets, the shared
SLIDES_DRIVE_ID / SLIDES_TEMPLATE_ID and the per-event SLIDES_FOLDER_ID_DEVCON8 /
PRETALX_API_KEY_WRITE_DEVCON8 as new repository secrets (the names carry the event, so
the test workflow cannot receive them by mistake). The permissions pass and the
/presentation/devcon8/ redirect exist already.
Test run on test-devcon-8 (local only; that workflow has no Google credentials and
the event mirrors real Devcon 8 talks, so keep speakers out of it): in devcon-api/.env
set GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY (from the devcon-7 sync workflow
secrets), SLIDES_EVENTS=test-devcon-8, SLIDES_DRIVE_ID=<AV shared drive>,
SLIDES_FOLDER_ID_TEST_DEVCON_8=<test folder in it>, SLIDES_TEMPLATE_ID=<DC8 template>,
PRETALX_API_KEY_WRITE_TEST_DEVCON_8=<write token scoped to the test event>,
SLIDES_SKIP_PERMISSIONS=true, and optionally SLIDES_ONLY_CODES=<a few codes>. Then
pnpm sync:pretalx:test; decks appear in the folder as <title> [<code>] and the URLs
land in data/sessions/test-devcon-8/*.json, which should not be committed.
Rewritten 2026-09-18 with the AV team's own template. Before this the script deleted and rebuilt every tab on each Pretalx publish, wiping the crew's notes (old §5.1).
Shape. One Google Sheet per day and stage. devcon8 (AV_TREE_EVENTS) puts it in the AV
team's Drive next to the intro cards, <AV folder>/$$EVENT/<Room>/<Day N - Mon D>/Run of Show · <Stage> · Day N, with the folder names shared with av-assets.ts (utils/av-drive.ts);
other events use <ROS_FOLDER_ID>/Day N · ddd D MMM/<Stage name>,
each copied once from the AV template (ROS_TEMPLATE_ID, the team's Google Sheet with
its RUN OF SHOW and AV & TECH CUES tabs). The template's header row tells the
script which layout it is: v1 is the AV team's original template, v2 the
proposal built 2026-09-18 (ownership shown in header colours, ACTUAL START and Δ,
REC from the Pretalx "do not record" flag, TRACK, checkboxes, a CUE TYPE link column
and a cue tab linked by CUE REF), v3 the AV team's revision of v2 (2026-10-20, see
below). Each generated sheet is recognised by its own header row (row 5 for v1/v2,
row 2 for v3), so sheets keep working with the layout they were created from; layouts can
live in one folder, which is how the side-by-side preview was made (ROS_ROOMS=main-stage
with the v1 template, ROS_ROOMS=stage-1 with the v2 template). Layout definitions are
in utils/run-of-show.ts, the format and clearing details per layout in LAYOUT_IO. Files are recognised by Drive
appProperties (ros_event, ros_day, ros_room), so renaming a file or a Pretalx
room is harmless. Each sheet also gets a CHANGES tab and a hidden _style tab (an
untouched copy of the template used to re-apply row formats).
v3 (2026-10-20). Agreed with the AV team: a DAY / STAGE line, then headers on row 2.
Columns # · STATUS · START · DUR · END · Q&A · MC · SESSION · SPEAKERS · SPEAKER CONTACT · FORMAT · SLIDES · DECK OK? · INTRO ANIMATION · STREAMING · REC · MODERATOR · MIC · NOTES · PRETALX ID. Dropped from v2: ACTUAL START, Δ, TRACK, ON SITE?, the cue sheet and
the MORNING / AFTERNOON / END OF SHOW rows (the REMOVED section still appears when a
session leaves). Rules that differ:
DURis the Pretalx slot, Q&A included (the AV timer table: Talk 20 + 5, Lightning 5 + 2, Mixed 25 + 0, Workshop 50 + 0; the buffer is the gap to the next slot).Q&AandSLIDESare fill-once: the sync writes them only while empty (Q&A from the timer table, other formats 0), then they are the crew's ("Laptop", a longer Q&A). A deck link that later changes in Pretalx is logged on CHANGES and highlighted, never applied.MCis a formula: minutes from END to the next live row's START (red when negative).SPEAKER CONTACTis synced: Telegram from Pretalx (speakers fetched with contacts, so it is only filled from the Pretalx source), else X, Farcaster or website, one line per speaker.- The stage's Q&A QR (
devcon.org/qr/meerkat/<room id>, redirected by devcon/api/meerkat/to the session a Meerkat moderator put live there, else the schedule's) is not a column:av-assets.tssaves it as$$EVENT/<Room>/Q&A QR - <Room>.png(one per stage, for offline use), and the sync links that file from the Digital Assets tab's "Q&A QR code" row. STREAMING,INTRO ANIMATIONare the crew's. The template'sDigital AssetsandLivestreaming Informationtabs are the AV team's and never touched.
Contract with the AV team. The sync owns columns B..K (STATUS .. SLIDES / MEDIA)
plus the key column PRETALX ID (R, appended to the template's 17). Columns L..Q
(MIC CONFIG .. LAST EDITED BY) are the crew's and are carried over by Pretalx code:
- a session moved within a sheet, to another stage or to another day takes its L..Q values with it (rows are indexed across every sheet before writing);
- a session that leaves a sheet is parked at the bottom under "REMOVED FROM THIS RUN OF
SHOW" as
CANCELLED, notes intact; theCHANGEStab says where it went; - rows the crew adds by hand without a
PRETALX ID(doors open, breaks) are kept and slotted by theirSTART TIME; - a hand-set
TBC,STANDBYorDELAYEDsurvives a resync;MODERATORandSLIDES / MEDIAare carried unless the sync has a value (the Google Slides deck URL from §2d lands inSLIDES / MEDIA); - every difference is appended to
CHANGES(run, code, session, field, from, to) and the changed cells are highlighted until the next run; #is a runningCOUNTAformula,END TIMEis the template's start + duration formula (v2 adds Δ and the CUE TYPE link),STATUSandFORMATkeep the template dropdowns (FORMATextended with the Pretalx types; v2 lists are strict), deck URLs render as aSlideslink, rows auto-fit their wrapped text. Rows are rewritten in place below the header (never deleted), so the template's column-wide rules survive. All conditional colours are owned by the sync and re-created every run, because pasting row formats strips them (v1: STATUS; v2: STATUS, REC, Δ, "on stage now", "speaker not on site", banding). A new sheet gets the event time zone (the live rules useNOW()), and v2 gets the stage's stream link from the room data.AV & TECH CUESis emptied of the template's example content once, when the sheet is created, and never touched again. Dividers: MORNING before 13:00, then AFTERNOON, EVENING from 18:00.
Merge rules are pure and unit-tested in
utils/run-of-show.ts;
generate-run-of-show.ts
does Drive/Sheets I/O only.
Running it. ROS_TEMPLATE_ID (the v3 template, also a repo secret) and the top folder
are required (no defaults): AV_DRIVE_FOLDER_ID_DEVCON8 for devcon8 (the AV Drive, same
secret as av-assets), ROS_FOLDER_ID for the other events;
ROS_SOURCE=data reads devcon-api/data/ instead of Pretalx (ROS_DATA_DIR to point
at a scratch copy), ROS_ROOMS limits rooms, ROS_DAYS limits days (1-based numbers,
counted over the whole event), ROS_DRY_RUN=true plans without writing.
ROS_ALLOWED_EVENTS in the script hard-limits which events may be generated; devcon8
was added 2026-10-20 and all its sheets were generated that day (38 sheets, 10 stages over
4 days). A local .env may still carry ROS_ROOMS from the devcon-7 preview: pass
ROS_ROOMS= to lift it. pnpm run-of-show:devcon-7 is the preview from committed data;
pnpm run-of-show:test runs test-devcon-8 from Pretalx. Google credentials: the shared
GOOGLE_* pair (the devcon8 workflow too, the account that has the AV Drive); ROS_GOOGLE_*
is only a fallback when GOOGLE_* is unset.
Drive gotchas found while building this. The service account is external to the EF Workspace. Files it uploads into an EF shared drive get the "EF Internal" classification within about half a minute and disappear from its view (not trashed, just no longer visible), which is what the "vanishing files" of §2d were. Files it copies and folders it creates are fine, so the template must be put in the folder by a person and the script only ever copies it. The account is a folder-level member, not a drive member, so the script walks the folder tree instead of searching the drive.
Open with the AV team. Whether stages without AV (breakouts, classrooms) need
sheets (ROS_ROOMS), whether FORMAT should map Pretalx types onto their list instead
of extending it, and who fills AV CUE REF on new rows (left blank today).
Ranked by severity. Each is a separate fix.
| # | Issue | Location | Effect |
|---|---|---|---|
| 1 | ✅ FIXED 2026-08 (§12) - updateEventVersion('devcon-7') hardcoded in the AV ingestion endpoint |
sessions.ts |
Every DC8 video PUT bumped DC7's cache-bust token; DC8 clients never saw new videos. |
| 2 | devcon8 rooms have no youtubeStreamUrl_* / translationUrl fields (re-verified 2026-08-10; the rooms were renamed in Pretalx and re-synced 2026-09-30, so the ids are now lotus-stage, lotus-1 to lotus-3, jasmine, lightning-stage, cls-stage, music-stage, workshop-2, workshop-3) |
devcon-api/data/rooms/devcon8/ |
All DC8 sessions render "No livestream available". |
| 3 | ✅ RESOLVED BY DECISION 2026-08-05 (§2c, §12c) - day→stream mapping hardcoded to Bangkok + Nov 12–15 2024 | devcon-app/.../sessions/index.tsx |
DC8 ships on event-app, so this devcon-app hardcode is moot and stays as-is. event-app's replacement uses event-relative UTC-day indexing off event.startDate. Caveat: the math assumes startDate stays midnight-UTC form - changing it to local-midnight shifts every stream index by one. |
| 4 | ✅ FIXED 2026-08 (§12) - PRETALX_QUESTIONS_* IDs were unmapped for devcon8 / test-devcon-8 |
config.ts |
Speaker socials, expertise, audience, tags, keywords all silently empty. |
| 5 | ✅ FIXED 2026-08 (§12) - submission_type numeric IDs were hardcoded to DC7's |
pretalx.ts |
DC8 sessions fell through to the raw Pretalx type name, breaking type filters. |
| 6 | ◐ INFRA REPLACED 2026-08 (§12d), asset swap remaining - social cards were entirely DC7 Bangkok-branded | devcon/public/social/dc8/ + devcon/src/pages/api/social/ |
The rendering stack was rebuilt on devcon.org with a Supabase cache; the DC7 art was copied to dc8/ as placeholders by explicit decision. Remaining for the rebrand: swap the dc8/ PNGs (same filenames, drop-in) and update the hardcoded "Bangkok, Thailand / 12-15 Nov 2024" block + Asia/Bangkok timezone in the two card templates. |
| 7 | ✅ FIXED 2026-08 (§12) - run-of-show rendered times in UTC | generate-run-of-show.ts |
Wrong wall-clock for stage crew (IST is UTC+5:30). |
| 8 | ✅ FIXED 2026-08 (§12) - stats-video.ts was hardcoded to devcon-7 + Nov dates |
stats-video.ts |
The only AV coverage report couldn't run for DC8; now pnpm stats:v <eventId>. |
| 9 | yt.ts uses @google-cloud/local-auth (interactive browser OAuth) |
google.ts |
Cannot run in CI; YouTube push is manual-only. Nuance: a service-account path exists (AuthenticateServiceAccount, used by import-yt.ts) but service accounts can only read YouTube - writes (thumbnails/titles) need the channel owner's OAuth, so CI would require a stored refresh token. |
| 10 | ✅ FIXED + VERIFIED (2026-08-13) — end-to-end test now passes: PUT /sessions/sources/:id on test-devcon-8 returns 204 and the [skip deploy] commits land on main. Three bugs were found and fixed along the way: (1) SessionToJson assumed DB-era comma-string tags/keywords and 500'd on the file-store's arrays; (2) session ids are NOT unique across events (test-devcon-8 is a devcon-7 clone) and reads resolved via sessionMap (last event wins) while writes used array findIndex (first event wins) — an AV write briefly wiped a devcon-7 archive session's AV fields (restored same day); updateSession now resolves through the same map as getSession; (3) commits serialized the hydrated slot_room object into data files — now stripped. Verify with pnpm av:test-write (devcon-api) — it aborts if the test session resolves outside test-devcon-8. TOKEN MIGRATED OFF PERSONAL ACCOUNTS (2026-08-13): Render GITHUB_TOKEN is now a classic PAT on the devcon-website machine account (scope public_repo only; the account holds Write on the monorepo). Verified end-to-end: pnpm av:test-write returns 204/204, both [skip deploy] commits land on main, and the commits are committed by devcon-website — proof Render uses the machine credential, not a person's. The token can also read the Actions API (all 4 sync-pretalx workflows visible), so webhook-driven workflow_dispatch is covered. No individual's offboarding can break the AV pipeline any more. Also fixed while testing: two writes to the same session within seconds could 409 on a stale contents-API sha and surface as a 500 — CommitSession now re-reads the sha and retries (deployed, verified). REMAINING: PRETALX_API_KEY(_MUMBAI) repo secrets still need a confirmed owner; set a calendar reminder for the token's expiry. Recommended follow-up: regenerate test-devcon-8 with prefixed session ids so cross-event id ambiguity disappears entirely. |
github.ts (TriggerWorkflow and CommitSession), via GITHUB_TOKEN in the API's Render env (the workflow files themselves use the repo-scoped secrets.GITHUB_TOKEN, which is fine) |
Webhook→workflow triggering and AV session commits die when the account is deprovisioned. PRETALX_API_KEY(_MUMBAI) repo secrets need an owner too. |
| 11 | ✅ FIXED 2026-10-09 (§12g) - Meerkat schedule sync ping was gated to devcon-7 and hardcoded to the retired meerkat.events URL |
sync-pretalx.ts |
DC8 schedule publishes never notified Meerkat (session Q&A), so its session list went stale. The sync now pings Meerkat's per-event Pretalx sync endpoint for test-devcon-8 and devcon8 (see docs/meerkat.md). |
| 12 | ✅ FIXED 2026-08 (§12) - devcon8 event metadata was empty |
devcon-api/data/events/devcon8.json |
The API served a nameless, dateless DC8 event; now hand-authored (title, dates, venue). Event metadata has no sync - it stays hand-authored. |
Since §12d (2026-08-05), the cards render on devcon.org with a Supabase render
cache. yt.ts
fetches devcon.org/api/social/av/{id} at 1920×1080 and pushes it straight to
youtube.thumbnails.set() - that route is the video thumbnail renderer, not just an
OG endpoint. Idempotency ledger:
youtube-thumbnails.json
(581 IDs), throttled .slice(0, 105) per run for YT quota.
Current route inventory:
| Route | Size | Consumed by |
|---|---|---|
devcon.org/api/social/av/[id] |
1920×1080 | yt.ts → YouTube thumbnails |
devcon.org/api/social/schedule/[id] |
1200×630 | devcon-app session share cards (?v= cache-bust); devcon.org Hero.tsx schedule card |
devcon.org/api/social/schedule-u/[id] |
1200×630 | personal-schedule share cards |
devcon-social.netlify.app/[name]/opengraph-image |
ticket | the original DC7 attendee "social ticket" (legacy, still on the old social-ticket app) |
devcon-social.netlify.app/av/[id]/opengraph-image |
1920×1080 | archive video OG (legacy, old app) |
The devcon.org routes live in
devcon/src/pages/api/social/
on the shared
og-cache.ts
service (details in §12d). They default to https://api.devcon.org - the old app's
localhost:4000 API_URL default footgun is gone for these routes, but still applies
to the legacy social-ticket app while it hosts the two remaining routes.
Cross-link with blocker #1: devcon-app cache-busts the schedule OG URL with
?v=${useEventVersion()}. Before the §12 fix, PUT /sessions/sources/:id bumped the
wrong event's version, which also kept DC8 share cards pinned to stale renders - both
sides of that are resolved.
yt.ts also contains a currently-disabled YouTube title + description rewriter
(syncDescriptions(), commented out in main()): it retitles videos to fit
<title> by <speaker> | Devcon SEA into YouTube's 100-char limit and regenerates
descriptions from session data (ledger:
youtube-descriptions.json,
578 IDs). If revived for DC8: the ' | Devcon SEA' suffix, the sessions/devcon-7
source, and the description boilerplate ("Devcon SEA was held in Bangkok… Nov 12 -
Nov 15, 2024") are all hardcoded.
Historic duplicate:
sessions.ts
(GET /sessions/:id/image) launches Puppeteer to screenshot a Handlebars
template, and has an unreachable 1920×1080 'video' branch (imageType hardcoded
'og') - someone intended it to do exactly what the av card route now does.
puppeteer pinned at 18.2.1, DC6-era track names. Dead.
- ✅ FIXED 2026-09-18 (§2e) - Run-of-show destroyed the AV team's manual work.
The old script unmerged and rewrote all formatting and rebuilt every tab on each
run. It is now a keyed upsert: only columns B..K and the
PRETALX IDkey are written, the crew's columns L..Q travel with the session, and nothing is deleted silently. Residual risk: last write wins if someone is typing in L..Q at the second a publish fires, and hand-applied cell colours do not travel with a moved row (values do). - Sync can delete committed data.
sync-pretalx.tsunlinkSyncs any local room/session file whose id is absent from the Pretalx response. A partial API response deletes real data. Partly mitigated byconcurrency: cancel-in-progress: falseandpull --rebase --autostash -X theirs. - Livestream config survives only by spread order. Room stream URLs persist across
sync purely because of
{...roomFs, ...room}insync-pretalx.ts(Pretalx never returns those keys). Reversing that spread silently wipes livestream config. - ✅ FIXED 2026-08 (§12) A schedule publish used to drop AV enrichment from the
serving copy until the next redeploy - certain to bite during the event.
replaceEventSessionsnow carries the enriched fields over (see §12). - ✅ FIXED 2026-08 (§12) A partial enrichment
PUTused to erase the rest - now patch semantics (§2a Path A).
Counts re-verified 2026-08-10 (devcon8 and test-devcon-8 rows: 2026-10-09):
| Event | Sessions | YouTube | IPFS | Swarm | StreamEth | Livepeer | Transcript |
|---|---|---|---|---|---|---|---|
| devcon-0 … 6 | 1,079 | ~all | ~all | ~all | – | – | – |
| devcon-7 | 650 | 580 | 0 | 555 | 388 | 1 | 367 |
| devconnect-arg | 418 | 418 | 0 | 0 | 0 | 0 | 0 |
| devcon8 | 316 | 0 | 0 | 0 | 0 | 0 | 0 |
| test-devcon-8 | 11 | 0 | 0 | 0 | 0 | 0 | 0 |
devcon8got its first synced sessions with the first schedule release on 2026-10-09 (316 sessions, 320 speakers, no AV sources yet).test-devcon-8carries the seeded test sessions from §12c and a few mirrored DC8 talks; its Open Tech Summit mirror was removed the same day (§12g).- IPFS mirroring stopped after DC6, and the gateway in use
(
cloudflare-ipfs.com,archive/.../Video.tsx) is decommissioned - so the IPFS tab is broken even for the videos that have hashes. - Swarm is the surviving decentralized mirror (555/650 for DC7), but the archive's Swarm player tab is commented out - only reachable in devcon-app.
- Livepeer is 1 session.
STREAMING_URL = 'https://live.devcon.org/'still exported fromdevcon/src/utils/constants.tsanddevcon-app/src/utils/constants.tswith zero consumers.
- Slides never render for any session.
archive/.../Video.tsxreadsvideo.slidesUrl; the API servesresources_slides.slidesUrlappears in zero session JSON files. 527 DC7 + 287 DC6 sessions have slides that are invisible.archive/src/types/index.tsis entirely Gatsby-era naming. - Related videos always empty.
store.tsintentionally skips vectorization with the comment "no client uses it" - butarchive/src/services/devcon.tsdoes call/sessions/:id/related. The comment is wrong; the archive silently gets nothing (live: 404, re-verified 2026-08-10). The working replacement isdevcon-ai/src/routes/recommend.ts. - Player switcher hidden.
devcon-app/.../sessions/index.tsxgates the source pills onsources_swarmHash, so YouTube+StreamEth sessions offer no choice. - New events invisible in archive search until
archive/src/hooks/useArchiveSearch.tsis hand-edited (hardcoded event list). - Archive playlists are dead code.
archive/src/services/playlists.ts- 24 JSON files are never imported by any route, and use the old Gatsby path
scheme.
Video.tsxrenders aplaylistsprop the page never passes.
- 24 JSON files are never imported by any route, and use the old Gatsby path
scheme.
- ✅ FIXED 2026-08 (§12b) Room ids collide across events in the store.
roomMapwas keyed by bare room id, but ids repeat across event folders, so whichever event's folder loaded last silently overwrote the others - live on production at the time (DC7 main-stage sessions served another event's room and lost theiryoutubeStreamUrl_*config). Fixed by stamping rooms with their event folder and namespacing the map key toeventId/roomId. - Session ids have the same latent collision -
sessionMapis keyed by bare slug, and 5 duplicates already exist (test-devcon-8 mirrors 4 DC7 talks;securing-ethereumexists in devcon-1 and devcon-7), last-loaded wins. Harder to fix than rooms becauseGET /sessions/:idis a public bare-id contract (archive/app deep links). Not fixed; needs a decision (e.g. prefer the newest event on conflict, or event-scoped lookups). Since 2026-10-08 the sync at least reports it: a same-event collision (two talks with one title) and a cross-event one land in the run's problems list. The first Devcon 8 sync (2026-10-09) listed 43 ids that also exist intest-devcon-8, the Open Tech Summit sessions mirrored there (§12g); a bare-id lookup for those resolved to whichever event loaded last. Cleared the same day: the 43 test proposals were deleted and test release 0.36 synced them out. - Production
/eventspublicly serves three phantom events (re-verified 2026-08-10):0(straydata/events/0.json, an artifact ofimport-yt.tsensureEventFile()),devcon-mumbai-playground(test instance, removed fromPRETALX_INSTANCESbut its data files remain), andtest-devcon-8.initStoreloads every file indata/events/- there is no draft/hidden flag. The archive is only shielded by its own hardcoded event list (§7.4).
- 1,092 whisper.cpp transcripts in
devcon-api/data/transcripts/devcon-{0..6}/(ggml-base.en, full timestamp arrays). Zero readers - no import, route, or build step touches that directory. - ~280 MB of DC7 per-room live-caption CSVs in
data/transcripts/devcon-7/-transcriptions-<room>.csv, one per stage, withRecognitionplus 10 translation columns (bn-IN, fil-PH, hi-IN, id-ID, km-KH, ms-MY, my-MM, th-TH, vi-VN, zh-CN). Raw AV-booth output. Nothing joins them back to sessions byslot_start/slot_end.breakout-1/breakout-2are header-only (nothing captured). transcript_vttis never parsed. No.vttparser, no<track>element, no caption rendering anywhere in the monorepo. Some values are the literal string"No VTT link provided". Onlytranscript_textis used, and only for LLM consumption.- Transcripts are not in the current RAG index.
devcon-ai/scripts/sync-sessions.tsbuilds embeddings from title/speakers/tags/description only; it does carryyoutube_idin metadata. The DC7 transcript corpus is reachable only via the olderdevcon-apiOpenAI Assistants vector store. devcon-api/generated/- 1,466 committed files (_youtube.txt×413,_1080.png×517,_social.png×519) of DC6 lower-thirds and social cards, produced bygenerate-images.yml(hourly cron, Node 14) runningyarn scripts:generator- a script that exists in nopackage.json, in a nested.githubGitHub never executes. Fully broken; artifacts stale.devcon-api/data/audio/devcon-6/- 7 mp3s (DC6 opening ceremonies) poweringGET /rss/podcast. Abandoned experiment.devcon-api/data/edge-cases.json- hand-maintained notes on YouTube playlists thatimport:ytcouldn't ingest (channel pages instead of playlists, etc.). Zero code readers; useful context if the devconnect-arg import is ever re-run.
devcon-api/src/scripts/encrypt.ts- not inpackage.json,data/accounts/gone.devcon-api/src/services/email.ts- nodemailer SMTP sender with three templates (incl.accreditation-confirmation); zero importers insrc/. Leftover from the removed account system.devcon-api/src/utils/{profile,zupass,web3}.ts- zero importers each; more account-system remnants (utils/account.tsby contrast is live, used by the Pretalx and DIPs clients).devcon-api/src/scripts/slides.ts-data/slides/doesn't exist, somigrateSlides()throws;exportSlides()commented out.devcon-api/src/clients/recommendation.tsdata/vectors/- deliberately disabled.
devcon-api/src/types/schedule.ts- Prisma-shaped{connect:{id}}payloads, fossilized; live replacement ispretalxToStoreData()inhooks.ts.devcon-api/src/services/at-slurper/main.ts- hardcoded mock events + Notion DB id, and it runs on API boot (imported for side effects incontrollers/at-slurper.ts).atproto-slurper/slurper/server.ts- firehose call commented out in both branches; hardcoded cursor override in millis where Jetstream expects microseconds;backfillData()fully commented out. Only the HTTP endpoints are live.devcon-ai/src/routes/generate-image.tsbase-model.ts-readFileSyncreference PNGs (mumbai-character.png,base-model.png,dc8-bg.png) that don't exist → ENOENT on any request.generate-image.tsis the sole consumer of@imgly/background-removal-node.
devcon-ai/src/lib/replicate.ts- zero importers,AVATAR_PROVIDERreferenced nowhere,replicatenot inpackage.json.- Workflows (
/.github/workflows/):run-of-show-devcon-mumbai-playground.ymlandsync-pretalx-devcon-mumbai-playground.yml(scripts don't exist; instance removed fromPRETALX_INSTANCES),ai-content-prep.yml(on: pushcommented out,yarnin a pnpm repo),devcon-db-cleanup.yml(no DB any more). (devcon-archive.yml, listed here previously, has since been deleted.) devcon-app/src/pages/streams.tsx- ops stream wall, gated onyoutubeStreamUrl_4(DC7 day 4).devconnect-appstages pages - replaced by stubs linking todevconnect.org/#videos; real code sits inpage_backup.tsxwith ~20 commented-out YouTube URLs from live hand-editing.
Before DC8 content exists - unblock the pipeline:
- ✅ DONE 2026-08 (§12) - the
'devcon-7'hardcode and the webhook memory-merge gap (§5.4) are fixed; the enrichment path is solid (§11.5). - ✅ DONE 2026-08 (§12) -
PRETALX_QUESTIONS_*andsubmission_typeIDs mapped fordevcon8andtest-devcon-8(#4, #5). - ✅ DONE 2026-08-13 -
GITHUB_TOKEN(Render) migrated to thedevcon-websitemachine account and the AV write path verified end-to-end (#10). Still to do: reconfirm ownership of thePRETALX_API_KEY(_MUMBAI)secrets against cfp.devcon.org. - ✅ DECIDED 2026-09-18 (§2e) - run-of-show mutability: the sync owns columns B..K,
the AV team owns L..Q, keyed by Pretalx code. ✅ 2026-10-20: devcon8 is in
ROS_ALLOWED_EVENTSwith the AV team's v3 layout, sheets generated in the AV Drive; the AV team reviews it on real data and the layout gets one round of changes. - ✅ DONE 2026-10-09 (§12g) - Meerkat's per-event sync endpoint is pinged for test-devcon-8 and devcon8 (#11).
Before DC8 doors open - livestream surface:
6. ✅ DONE 2026-08-05 (§12c) - event-app carries DC8's AV surface (§2c). Schemas
extended, livestream/recording embed + translation link + room-screen stream QR
built and verified headlessly. devcon-app's #3 hardcode left as-is (moot).
7. Event metadata: ✅ DONE 2026-08 (§12) - devcon8.json authored (#12). Room
stream fields (#2) still pending (need the DC8 YouTube channels).
8. ◐ HALF DONE 2026-08 (§12d) - social-ticket replaced by cached devcon.org routes;
remaining: swap the dc8/ placeholder art + the hardcoded DC7 location/date/timezone
in the card templates (#6).
9. ✅ DONE 2026-08 (§12) - run-of-show UTC → event-local time (#7).
10. ✅ DONE 2026-08 (§12) - stats-video.ts takes an event id (#8).
Independent of DC8 - cheap credibility wins:
11. slidesUrl → resources_slides (§7.1). One-line fix, unlocks 814 sessions' slides.
12. Either build the /related vectors or point the archive at
devcon-ai/recommend (§7.2).
13. Swap the dead cloudflare-ipfs.com gateway (§6).
Guiding constraint: during the event the infra must not go down, no matter what - Pretalx being slow or venue internet dropping are expected, not exceptional.
-
Keep the serving architecture as is. Its accidental genius is that nothing at runtime depends on Pretalx: the API serves from memory, boots from git, and only talks to Pretalx on a schedule publish. If Pretalx is slow or down mid-event, nothing user-facing degrades - updates just wait. A database or a live-Pretalx dependency would both be steps backward on availability. Client-side resilience is covered by event-app's offline-first design (Dexie + SWR).
-
"Pretalx as source of truth" - adopt as an editing surface, not a serving path. Pretalx custom fields are fine for anything decided before the event (room stream URLs, translation URLs), ingested at sync time - that would also kill the hand-edited room-JSON overlays (#2). But never let a consumer read Pretalx at runtime (this includes not activating event-app's dormant direct-Pretalx provider for the event).
-
Keep
PUT /sessions/sources/:idfor day-of enrichment. It updates memory instantly, commits to git for the next boot, doesn't touch Pretalx, triggers no resync, and can't collide with the run-of-show sheet. Moving day-of enrichment into Pretalx doesn't fit its update model: the webhook only fires on schedule publish, so real-time updates would mean constant re-publishes (each one a full resync that also destroys the AV team's run-of-show edits, §5.1) or a new answers-poller - more infra, not less. -
Rejected: dual-writing enrichment to Pretalx custom fields. Technically possible without a resync (
POST/PATCH /api/events/{event}/answers/), but: it puts Pretalx in the enrichment hot path (fail the PUT, or build fire-and-forget retry machinery); two writable copies drift, and the sync's{...fsSession, ...session}precedence means a stale Pretalx answer would clobber a newer git correction on every publish; and it needs an organizer-scoped token in the API's env - a much bigger blast radius than the current narrow key. Git history already provides the durability argument. -
The two code fixes that matter for event days: the
'devcon-7'version-bump hardcode in the enrichment endpoint (#1) and the memory-merge gap (§5.4). Both are small; together they make the enrichment path genuinely solid. ✅ DONE 2026-08 (§12), plus patch semantics on the enrichment PUT (§5.5). -
Social/OG images: keep on-demand rendering. The cards are pure derived artifacts (title + speakers + track art), regenerated automatically on change via the
?v=cache-bust; storing them upstream in Pretalx would only create a stale second copy, and Pretalx has no per-session image slot anything reads. Their failure mode is crawler-facing and cosmetic, not event-critical. If belt-and-braces is wanted later, the right "upstream" is pre-rendering to static files at sync time (the resurrected DC6 pattern, §8) - after the real blockers. ✅ Implemented 2026-08 (§12d) as the Supabase-cached variant on devcon.org: still on-demand, but render-once with stale-serving, deploy-proof storage, and no localhost default. -
Cheap availability insurance: Render is the only real single point of failure - consider CDN caching on the hot
GETendpoints for event days so even an API restart mid-keynote is invisible. ✅ IMPLEMENTED 2026-08-10 (§12e) on the origin side; ✅ ACTIVATED 2026-08-19 via Render Edge Caching (see §12e update) — hot GET routes now servecf-cache-status: HITfrom the edge. -
Repo weight is a deploy-time tax on everything (found 2026-08-17): a SHALLOW clone of the monorepo is ~1.2 GB -
devcon-api/data/alone is ~650 MB (Devcon 6 ceremony MP3s underdata/audio/, ~200 MB of DC7 transcript CSVs underdata/transcripts/) anddevcon/public/~540 MB (incl. a 17 MB presskit PDF). Every Render build and every Netlify build across all projects re-downloads this on every commit. When Render's clone/cache throughput degraded on 2026-08-14 (their side - phase went 1m43s → 10-27 min with identical repo content; support ticket territory), deploys jumped from ~5 min to 13-28 min, and the repo size is the amplifier. Recommendation: movedata/audio/+data/transcripts/to object storage (they're read-rarely blobs, not schedule data) and prunedevcon/public/- halves every clone in the org. Not urgent for DC8, but do it before the repo grows another gig of DC8 media.
-
Slides for DC8: keep the per-talk Google decks, keep them private until after the event, and enforce it (§2d). Keep the decks because the AV team needs every deck URL in advance to pre-load and cue, a deck that exists from day one is why 527 of ~650 DC7 sessions ended with slides, and the archive fills itself. Keep them private because that is what DC7 did in practice (§2d timeline: slide content reached the public only in December) and it removes the pre-talk leak question entirely. What was wrong last year is that privacy relied on nobody touching a deck's sharing setting, and grants only ever accumulated. The URL being public everywhere (API, repo, one swap from the social link) is fine as long as read access is what the pipeline controls. Desired state, enforced by a scheduled permissions pass (an extension of
runPermissions()pluspermissions.list), run daily during the event and on every sync:- Speakers: writer from creation, as today. Revoke when a speaker is removed from the talk in Pretalx.
- AV team: reader from creation, through one Google Group (or shared-drive membership), so they hold URL and content ahead of everyone else.
- Public: nothing until the event is over. Any
anyonegrant found on a deck during the event is removed, whoever added it. A speaker can re-share their deck, andwritersCanShareis not available for shared-drive files, so reconciliation is the only control. - Never
anyonewriter, before or after the event. Strip it whenever found. - Publish once, after the event: a single scheduled run exports every deck to
PDF into
resources_slideswith the existing export code, so later speaker edits do not change the on-record version, and optionally addstype: anyone,role: reader,allowFileDiscovery: falseon the decks themselves. This replaces the manual December 2024 pass. Prerequisites: confirm the Workspace or shared-drive policy allows "anyone with the link" sharing if the decks themselves are to be opened after the event (per-user external grants already work, link sharing is a separate setting); lift thedevcon8is inSLIDES_ALLOWED_EVENTSsince 2026-09-30; each run still opts in withSLIDES_EVENTS=devcon8and needsSLIDES_FOLDER_ID_DEVCON8andPRETALX_API_KEY_WRITE_DEVCON8(a token scoped to the devcon8 team, verified 2026-09-30: it can create questions and answers on devcon8 and nothing on the test event). The three slides questions exist ondevcon8since 2026-09-30 and their ids are in the devcon-api config and the/presentation/redirect page, sodevcon.org/presentation/devcon8/<code>/resolves as soon as a deck link is written. Alternative if stage AV does not project from the decks and the Drive infra should go: a private Pretalx file question (not the native Resources feature, which is public on the talk page as soon as the schedule is), released by the sync intoresources_slidesafter the event, since anything in a session JSON is public on landing and the Pretalx file URL is unauthenticated once known. Either way the archive needs theslidesUrlfix (§7.1) or nothing renders.
All in devcon-api/;
every behavioral change carries a test that failed before the fix (TDD), full suite
19 tests green, tsc --noEmit clean. Sections above are marked ✅ FIXED where they
describe pre-fix behavior.
Enrichment endpoint (src/controllers/sessions.ts)
- Version bump uses the session's own event (was hardcoded
'devcon-7', blocker #1). Why: every DC8 video PUT bumped DC7's cache token, so DC8 clients and the?v=OG-card cache-bust would never see new videos. - Patch semantics on
PUT /sessions/sources/:id(body.x ?? data.x ?? fallback, wasbody.x ?? ''- §5.5). Why: a vendor tool sending only a YouTube ID silently wiped the session's swarm hash, transcripts and duration. Omitted = keep, explicit''= clear; documented in the swagger comment. - Generic
PUT /sessions/:idbumps the event version too. Why: title/slides corrections updated memory but never told clients to refetch.
Resync safety (src/data/store.ts, src/controllers/hooks.ts)
- Enrichment survives the webhook resync in memory (§5.4):
replaceEventSessionsnow carriesENRICHED_SESSION_FIELDS(sources_*, transcripts, duration) over from the previous in-memory copy; Pretalx still wins for its own fields. Why: a schedule publish dropped all videos from the serving copy until the next redeploy - certain to bite during the event when publishes and enrichment overlap. - Unknown Pretalx slugs are rejected (400 + warning; the
|| 'devcon-7'fallback is gone). Why: a webhook from a new/renamed Pretalx event silently resynced devcon-7. The test mocks the Pretalx client and asserts the sync is never invoked.
Auth (src/middleware/apikey.ts)
x-api-keyheader accepted alongside?apiKey=. Why: query-string keys land in Render/proxy access logs; DC8 vendor keys should travel in the header.
DC8 configuration (src/utils/config.ts, src/clients/pretalx.ts, data/events/devcon8.json)
PRETALX_QUESTIONS_*mapped for devcon8 + test-devcon-8 (blocker #4), ids fetched live from cfp.devcon.org. Deliberate omissions are commented in the config: DC8 merged Website+Github into one question (mapped to WEBSITE), has Bluesky (170) instead of Lens, and no Keywords question; the test event also lacks Farcaster and Tags. Why: without these, speaker socials/expertise/audience/tags sync silently empty.- Featured speakers: an organizer writes
#featuredin the speaker's Internal notes in Pretalx (organizer-only, never shown to speakers). Until 2026-10-21 this was a "Featured speaker" speaker question, but Pretalx shows every active speaker question in the speaker's own profile and speakers ticked it themselves, so it was deactivated ondevcon8(#186) and its config removed. The sync collects the marked speakers into the event record'sfeaturedSpeakers(speaker files are shared across events, so the flag cannot live there), rewriting the list whenever the notes are readable, so it can shrink to empty. event-app lists them first and, while the list is empty, falls back to speakers with a featured session (Pretalx's ownis_featuredon the proposal, mapped asfeatured, a separate organizer-only flag). mapSubmissionTypeextended with DC8 + test-devcon-8 ids (blocker #5). Talk/Keynote →Talk, workshops (1h/1h30/2h) →Workshop, Mixed Formats →Panel, Lightning Talk →Lightning Talk. "Experience" (89/100) intentionally unmapped - a genuinely new format, better shown as itself than mislabeled. Verified end-to-end with a real sync of test-devcon-8 (types and expertise populate).devcon8.jsonmetadata authored (blocker #12): Devcon 8, edition 8, 3-6 Nov 2026, Mumbai, Jio World Centre (+ address/website/directions), key-parity with devcon-7 verified. Values sourced from devcon.org's own copy. Why: nothing fills this file automatically; the API served a nameless, dateless DC8 event.
Scripts (generate-run-of-show.ts, stats-video.ts)
- Run-of-show renders event-local time via
EVENT_TIMEZONES(devcon8 →Asia/Kolkata);main()guarded byrequire.main === moduleso the file is importable by tests (blocker #7). Why: the sheet is wall-clock for the stage crew and IST is UTC+5:30 - an offset UTC formatting can never produce. stats-video.tstakes an event id (pnpm stats:v devcon8) and derives days from the event file's start/end dates (blocker #8). devcon-7 output verified identical to the old script (51/107/126/52 sessions per day).
- Four new test files
(
sessions.test.tsx6 tests,hooks.test.tsx2,store.test.tsx1,generate-run-of-show.test.tsx2, since folded intoutils/run-of-show.test.ts, §2e); GitHub and Pretalx clients mocked, nothing touches the network. package.json: jestmoduleNameMapperfor the@//@lib/aliases (mirrors tsconfig 1:1) - without it no test importingappcan run at all.
Building the event-app AV slice surfaced a production bug; both the fix and the slice landed together:
- devcon-api: room-collision fix (§7.6).
GetRoomData()stamps each room with its event folder;roomMapkeys are noweventId/roomId, and every resolution site (boot, event rooms, updateSession, createSession, webhook resync) passes the session's event. Found because the new event-app livestream logic readslot_room.youtubeStreamUrl_2from the live API and got a playground room instead of DC7's main-stage. Regression test instore.test.ts; API responses now also carryeventIdon room objects (additive). The session-id twin of this bug (§7.7) is documented but not fixed. - event-app: first AV surface (the "schema + minimal player" slice).
SessionSchemagainedsources_{youtubeId,streamethId,swarmHash},RoomSchemagainedyoutubeStreamUrl_1..4+translationUrl(the zod models used to strip these at validation), the provider passes them through, and a newEventSchema/getEvent()/useEvent()exposes event start/end dates. NewSessionMediacomponent on the session page: recording embed (YouTube, else StreamEth) when sources exist; otherwise the room's stream for the session's conference day (derived from the event startDate - no day-of-month hardcodes - via the mockableuseNowclock) when the session is live or starting within the hour; otherwise nothing. Verified headless against devcon-7 data: recording renders, mocked-live session picks the correct day-2 main-stage stream, idle session renders nothing.
The §2c decision was made (event-app) and the surface completed the next day. All
changes verified by per-task review plus a whole-change review, and headlessly with
playwright (API stubs + ?mockNow): live session embeds the day's stream and shows
the translation link, a recording wins over the stream, the room screen renders the
livestream QR. tsc --noEmit clean.
- Shared day→stream helpers:
eventDayIndex(tMs, eventStartIso)andstreamUrlForDay(room, tMs, eventStartIso)extracted fromSessionMediaintoevent-app/src/components/schedule/utils.ts(UTC-calendar-day indexing, 1-based, bounded bySTREAM_FIELDS). - Room screens
(
RoomScreen.tsx): second QR ("Watch livestream") for the current day's room stream, same QR options as the session QR, anchored on now; hidden when no URL, no crash offline. - Translation:
SessionMediarenders a "Live translation available" link (plain anchor, new tab) while a session is live or starts within the hour and its room hastranslationUrl- with or without an embed. - Timezone validation: the UTC-day math was traced against DC7's
.add(7,'hours')convention - no off-by-one for IST session hours, providedstartDatestays midnight-UTC form (see the #3 caveat). - Test data:
data/rooms/test-devcon-8/{keynote-stage,stage-1}.jsonseeded with the real DC7 livestream VODs, one distinct video per day slot (keynote-stage carries DC7 main-stage days 1-4, stage-1 carries DC7 breakout-1 days 1-4; all eight verified alive via YouTube oEmbed,www.youtube.com/embed/...form), and DC7's real translation portal (https://stm.live/Mainstage, still live) astranslationUrl;data/events/test-devcon-8.jsongainedstartDate/endDate(copied from devcon8) - without themeventDayIndexreturns null and the seeded config is unreachable (caught by the final review; the verification stub had masked it).?dataset=test-devcon-8is now testable end to end. - Known accepted residual: the pre-existing bare-slug session collision (§7) means two DC7-mirrored slugs in test-devcon-8 can surface the seeded URLs if deep-linked during their November windows - root cause unchanged, deliberately not fixed here.
Decision context: §11.6 recommended keeping on-demand rendering; the requirement added was a strong cache that survives rebuilds. Implementation generalizes the ENS ticket route's proven pattern (render → jpeg → public Supabase bucket, 12h Last-Modified staleness, serve-stale-on-failure).
- New shared service
devcon/src/services/og-cache.ts:serveCachedImage()encodes the contract - bucket-first read, render+upsert on miss/stale, serve the stale copy when the render or devcon-api fails,x-og-cache: hit|render|staleheader, CDNs-maxage=43200, stale-while-revalidate=86400. Buckets auto-create (fixed en route: Supabase signals a missing bucket as HTTP 400 with "Bucket not found" in the body, not an outer 404). The existingapi/ticket/[...slug].tsxnow imports from this service, behavior unchanged. - Three new routes (Pages API, node runtime, satori templates lifted verbatim from
social-ticket, in
devcon/src/pages/api/social/):devcon.org/api/social/schedule/[id](1200×630),/api/social/av/[id](1920×1080 q85, the YouTube thumbnail source),/api/social/schedule-u/[id](personal schedule). Fonts and art preloaded fromdevcon/public/{fonts,social}/as data URLs - no runtime asset fetches. Speaker avatars prefetch with a blockie fallback so a dead avatar host cannot fail a card. - Consumers flipped: devcon-app session + personal-schedule share URLs (keeping
?v=as the crawler cache-buster), devcon.orgHero.tsxschedule card, andyt.tsthumbnails. The/[name]attendee card and the archive's OG remain on the olddevcon-social.netlify.appapp for now. - The cache of record is the
social-cardsSupabase bucket - external to Netlify, so deploys/rebuilds never evict it. Verified live: render→hit with byte-identical serves; cards publicly reachable at the bucket URL; with devcon-api fully dead, a previously-rendered card still serves 200 (uncached ones fail contained with 503). - Data-helper fix caught during the port:
GET /account/:id/schedulewraps its payload as{user}, not{data}. - No new env vars:
SUPABASE_*already on Netlify;DEVCON_API_URLoptional (defaults tohttps://api.devcon.org- the old app'slocalhost:4000default footgun is gone). - Remaining for #6: the actual DC8 rebrand - swap
devcon/public/social/dc8/PNGs (drop-in, same filenames) and update the hardcoded DC7 location/date copy andAsia/Bangkoktimes in the schedule/av templates.
The API served everything uncompressed (the full devcon-7 session list was 23.4 MB
per request, 1.6 MB gzipped) and, despite being proxied by Cloudflare, nothing was
edge-cached (cf-cache-status: DYNAMIC - no JSON endpoint sent Cache-Control).
This implements the §11.7 insurance: cached GETs keep serving through an API restart.
All in devcon-api/, tsc --noEmit clean, headers verified against a local boot:
compressionmiddleware insrc/app.ts(23.46 MB → 1.63 MB on the session list).- New
src/middleware/cache.ts-publicCache(maxAge)setsCache-Control: public, max-age=N, s-maxage=N, stale-while-revalidate=2Nat header-write time, GETs with <400 status only (webhooks, PUTs, ai and error responses stay uncacheable); handlers that set their ownCache-Controlwin. Cached routes getAccess-Control-Allow-Origin: *because Cloudflare ignoresVary: Origin; safe, all catalog consumers fetch non-credentialed. - TTLs: 60s on the hot catalog routes (sessions/speakers/events/rooms/version),
3600s on
/dips*+/rss/podcast,/static1 day,/data1 hour. 60s keeps day-of updates (schedule publish, AV enrichment PUT) propagating within ~a minute; edge + browser staleness don't stack (Date/Age). - Accepted nuance: devcon-app's version-poll refetch can re-read a ≤60s-stale
payload (refetch URL isn't cache-busted; the OG-card
?v=URLs are). Moot for DC8: event-app SWR-revalidates instead. - ✅ Edge caching ACTIVATED 2026-08-19 — via Render, not a Cloudflare rule
(issue #2649): devcon.org's DNS isn't on Cloudflare, so no Cache Rule was
possible; but Render's CDN is Cloudflare (
api-devcon.onrender.com.cdn.cloudflare.net), and its Edge Caching → "All files (advanced)" setting (on the api-devcon service — not devcon-podbox) gives the same semantics: it honors Cache-Control and caches nothing that saysno-store. To make that fail-closed, adefaultNoStoremiddleware (mounted app-wide inapp.ts) now stampsCache-Control: no-storeon every response that didn't set an explicit policy — otherwise header-less routes (/regenerate, at-slurper GETs, devabot threads) would have inherited Render's ~120min default TTL (frozen side-effect GETs, publicly cached per-user data). Also fixed:publicCachenow covers HEAD, socurl -sIprobes show real headers. Verified live:/sessionsMISS→HIT(ageheader present), 404sBYPASS. The edge cache auto-purges on every deploy, so data-sync commits still propagate normally. Note: cache HITs never reach the origin — Render request logs/metrics undercount real traffic now. - Optional if 60s is ever too slow: fire-and-forget Cloudflare purge in the two
real-time write paths (Pretalx webhook,
PUT /sessions/sources/:id); needs a cache-purge zone token in Render's env, deliberately not built yet.
Releasing a schedule from the Pretalx orga UI kept timing out (and the release rolled back — no version minted), while the API release endpoint worked. Root cause is a chain across both sides of the webhook:
- The pretalx-webhook-plugin fires its POST synchronously inside the release request (Django signals) with no timeout, and before the release transaction commits.
- Since the release-race guard (§2), devcon-api's webhook handler held the response open for the entire sync — 10s when visibility resolves fast, up to 2+ minutes when it doesn't. Release request = freeze + that wait → nginx 60s / gunicorn 30s timeouts kill the worker → transaction rollback → "timed out, nothing released". Pre-commit firing also means a rolled-back release can still trigger a ghost sync.
Fixes:
- devcon-api (
hooks.ts): webhook now acks 204 immediately and runsSyncPretalxdetached (waitForPendingSync()exposes it to tests). Deployed via normal push; this alone makes the deployed plugin harmless, since its blocking POST returns in <1s. Must stay 204 — plugin ≤0.1.5 only accepts 200/201/204. - Plugin 0.2.0 (committed, deploy pending): POST moved to a daemon thread
scheduled via
transaction.on_commit+(5, 180)stimeout — releases never block on the receiver and webhooks can no longer announce rolled-back releases. Deploy = bump the git pin incluster/devcon/pretalx/ansible/inventories/mumbai/group_vars/instances.yaml(currently21a8d2e; the pretalx pin itself moved on 2026-10-08, §12g) + ansible run. - Fallback while any of this is undeployed:
pnpm pretalx:release(devcon-api) releases via the API with auto-incremented version numbers.
Context: Open Tech Summit India (FOSSASIA) runs co-located with Devcon 8 on the CLS stage and publishes its own schedule on eventyay (a Pretalx fork). Its sessions had to exist in our Pretalx event like every other talk, so the app, the slides pipeline and the run of show see them, without the partner re-entering anything.
- Pretalx upgraded for the tags API (cluster repo,
instances.yaml): the pin moved from the launch commit to upstream071224513a(2026-03-31, six days later), the commit that fixes the submission serializer rejecting everytagswrite ("Invalid pk"). Deployed with ansible on both Mumbai nodes on 2026-10-08; the comment next to the pin lists the versions. Anything from v2026.2.0 upwards changes the API, so check the devcon-api clients before going further. The webhook plugin pin is unchanged (§12f). pnpm sync:eventyay <event> [--apply](sync-eventyay.ts): reads the partner's public widget JSON and creates or updates proposals ontest-devcon-8ordevcon8, matched on the source session code kept in an organiser-only "Sync source ID" question. Dry run by default, idempotent, never deletes (a vanished source session is reported). It maps the partner's session types to ours (CLS - Talk / Lightning Talk / Miniworkshop / Panel (Open Tech Summit)), its rooms (CLS Stage,CLS Stage - Builders Room) and its tracks to our tags, accepts and confirms, and places each proposal in the unreleased WIP schedule with the source duration. A session held several times becomes one proposal per run with a "(n/N)" title suffix, since our events allow one slot per proposal. Speakers: the partner contacts its own speakers, so no real email is involved. A speaker whose full name already exists ondevcon8ordevcon-7is attached as that account (gaps filled, nothing overwritten); everyone else gets a placeholder address on a team mailbox with a+suffix per source speaker (SYNC_PLACEHOLDER_EMAILin.env, never in the repo), so the profile exists with the eventyay bio, avatar (webp converted to PNG, Pretalx refuses webp) and social links (X, Telegram, one website/GitHub/LinkedIn link, into the speaker questions). Applied ondevcon8on 2026-10-08: 43 proposals on Nov 5, 66 speaker links. Pretalx queues an "added to a proposal" draft in the outbox for every attach; nothing is sent, the speaker team discards them.- Release guard:
pnpm pretalx:releaserefusesdevcon8(NEVER_RELEASE, no override). Publishing Devcon 8 schedules is the speaker team's act in the orga UI (§2). The whole flow was exercised ontest-devcon-8first (release 0.35, 2026-10-08). - Slides for partner tracks:
SLIDES_DELEGATE_TRACKS/SLIDES_PLACEHOLDER_DELEGATE(§2d). The 43 decks were created on 2026-10-08 withSLIDES_EVENTS=devcon8, each shared with the partner contact only (verified in Drive), deck links written to Pretalx. - Mapper (
clients/pretalx.ts, all events):descriptionfalls back to the abstract when Pretalx returns an empty string (the partner's sessions carry only an abstract); the "Personal Website, Github, or other relevant link" answer is now published, asgithubfor a GitHub URL on events without a dedicated GitHub question, aswebsiteotherwise. It was mapped but never written before, so the next sync also surfaces the links Devcon 8 speakers entered there. - Sync problems list now includes same-event and cross-event session-id collisions (§7.7).
- "Featured speaker" question on
devcon8(2026-10-09, §12 DC8 configuration; replaced 2026-10-21 by#featuredin the speaker's Internal notes, see above). - First Devcon 8 schedule release, 2026-10-09, by the speaker team in the orga UI.
The webhook dispatched
sync-pretalx-devcon8(success: 316 sessions, 320 speakers, 11 rooms committed, slides off as designed, 43 collision warnings againsttest-devcon-8) andrun-of-show-devcon8, which failed on the Google Sheets "Write requests per minute per user" quota while writing the first full schedule (four Sheets writes per tab, about 45 tabs, Google allows 60 a minute). Fixed the same day: every write is spaced to stay under the limit and retried with backoff on 429/5xx, verified with a full Devcon 8 run against a throwaway sheet (32 tabs, 3 minutes, no retries). Found on the way: both run-of-show workflows read the same repo secretROS_SPREADSHEET_ID, so the test release of 2026-10-09 rewrote the AV team's sheet with test data before Ceci's next releases half-rewrote it; the test workflow now readsROS_SPREADSHEET_ID_TEST_DEVCON_8, unset, so it exits before writing. - Meerkat ping for DC8 (#11): Meerkat now pulls the schedule from Pretalx itself and
exposes one public
POST .../api/v1/pretalx/<event>/syncper event (one sync a minute at most, answers 200 or 202).notifyClients()posts there fortest-devcon-8anddevcon8on every sync run (the path parameter is the Pretalx slug; Meerkat looks its conference up by it), keeps sending the bearer token whenWEBHOOK_MEERKAT_SECRETis set, and both workflows now pass that secret. Verified against the test event: Meerkat answered with the released version and the upserted and deleted codes (talks that leave a release are removed on their side too). Meerkat keys sessions by Pretalx code (the app's Q&A block already tries that first) and its stage is the slug of the room name, which equals our room id: the room screens sent the room name, so the "See questions" QR never showed and the presenter link 404ed; they send the id now (RoomScreen.tsx). Meerkat has one conference, "Devcon 8 Mumbai", still linked totest-devcon-8; the prod ping answers 404 until the Meerkat team relinks it todevcon8. - Test mirror cleaned up (2026-10-09): the 43 Open Tech Summit proposals were deleted
from
test-devcon-8through the API and test release 0.36 synced them out (11 test sessions left), so Devcon 8 syncs no longer report id collisions for them. The 43 test decks remain in the test Drive folder.
pnpm av:assets devcon8 [--upload] (av-assets.ts)
builds and syncs what the stage crew needs offline, and runs in the devcon8 sync
workflow after every schedule release (secret AV_DRIVE_FOLDER_ID_DEVCON8, the
Google service account, manifest committed as data/av-assets/devcon8.json):
- one 1920×1080 intro card per session (the
/api/social/av/render) under$$EVENT/<Room>/<Day N - Mon D>/Intro Cards/HH.MM <Title> [CODE].jpg, so files sort in running order; the AV card got the white logomark on every track and 160 px avatars on the footer baseline (dc8-social-card.tsx, AV size only); - one speaker photo per scheduled speaker in
Speaker Assets/Photos, 1024 px from the Pretalx original, else our 256 px mirror ((256px only)), else the blockie; - the list of speakers without a photo as a Google Doc, rewritten in place;
- (2026-10-20) one Q&A QR per stage,
$$EVENT/<Room>/Q&A QR - <Room>.png(2048 px, from the site's own/qr/meerkat/<room>?svg), linked from each run-of-show sheet (§2e); none for the Music Stage, which has no Q&A.
The manifest keys files by what they are (card:<code>, photo:<code>, qaqr:<room>), so a
rescheduled session's card is moved, a changed one rewritten in place, a stale one
trashed, and links stay stable. A file that fails to render in a run (a timeout or a 403
from devcon.org) is not stale: Drive keeps the previous one (fixed 2026-10-20, after a
burst of 403s trashed four cards; they were restored). Day folders are numbered over the
whole event, also with --rooms.
Drive labelling, and the trick that works around it. The EF shared drive labels
every file an external account uploads "EF Internal" within seconds and the label hides
the file from that account: the service account could add files but never see, update
or trash them again (and domain-wide delegation is not authorised for it). Copies of a
Public-labelled file inherit Public and keep it when their bytes are replaced, so every
file is created as a copy of _public placeholder (DON'T DELETE).png in
Speaker Assets/Photos and then overwritten with the real content; Google-native files
and folders are not labelled, so Docs and folders are created directly. Keep that seed
image where it is; if it moves, the script searches the AV tree before giving up.
Blockers #2 (production devcon8 room stream fields - needs the DC8 YouTube channels), #6 second half (DC8 asset swap + DC7 location/date/timezone copy in the card templates - infra done, §12d), #9 (YouTube OAuth; #11 Meerkat closed 2026-10-09, #10 closed 2026-08-13: AV write path + token migration verified), footguns §5.2-5.3 (sync deletion, spread-order fragility; §5.1's run-of-show rebuild is fixed, §2e). §12e's edge caching is now fully active (Render Edge Caching "All files", 2026-08-19). Added 2026-10-09 (§12g): CI still creates no decks for devcon8 (§2d checklist).
No code changes are proposed in this document. To validate the findings above:
- Coverage counts:
cd monorepo/devcon-api && pnpm stats:v <eventId>. - Room stream fields: inspect
devcon-api/data/rooms/devcon8/vsdevcon-api/data/rooms/devcon-7/main-stage.json. - Broken slides field:
grep -rl 'slidesUrl' monorepo/devcon-api/data/sessions/returns nothing;grep -rl 'resources_slides'returns 814 files. - Dormant recommender:
curl https://api.devcon.org/sessions/<id>/related→ 404. - Thumbnail rendering:
curl -I https://devcon.org/api/social/av/<sourceId>(check thex-og-cacheheader). - Pipeline liveness:
git log --oneline --author=github-actions -5shows[action] Pretalx Synccommits (the first(devcon8)one landed 2026-10-09). - Sync auth against cfp.devcon.org: run
pnpm sync:pretalx:devcon8locally - a stale base URL / key shows as public endpoints 200 but private events 401 (the redirect strips theAuthorizationheader, so the failure is silent). - Meerkat ping:
grep -n "meerkat" devcon-api/src/scripts/sync-pretalx.tsshows theMEERKAT_EVENTSmap (#11);curl -s --get --data-urlencode 'stage=<room id>' https://app.meerkat.events/api/v1/eventslists what Meerkat holds per stage.