Owner-only admin dashboard for hosting and sharing HTML decks through unique URLs. Built with Next.js (App Router), Prisma, and SQLite.
Make a deck anywhere, host it on your own machine or server, and share it from your own domain.
This is not a slide builder. The intended workflow is:
- Generate a deck anywhere you want, including with Claude, Codex, or your own tools.
- Reuse your own HTML slide template if you want consistent layouts, navigation, and interactions across decks.
- Export or write the result as standalone HTML, including clickable links, buttons, charts, and custom styling.
- Paste that HTML into Deck Manager and publish it behind a share link and optional password.
If Claude makes you a clickable HTML deck, you can drop it in here and it stays a clickable deck. You do not need to rebuild it as PowerPoint or convert it into a different slide format first.
If you already have a good HTML slide template, this works especially well: tell Claude or Codex to generate the deck inside that template, paste the result here, and share it with an optional password plus any additional resources link you want to attach.
For teams already using AI agents, Deck Manager can be the publish target in an automated loop: generate deck HTML, push it to your site, grab the share URL and password, and send it to the client without a manual formatting pass.
- 🔐 Owner login via Google OAuth and/or shared password
- 🗂️ Rich admin UI to create, edit, preview, and deactivate decks with inline HTML editor
- 🔁 One-click share link regeneration with immediate revocation of old URLs
- 🤖 Owner-authenticated admin API for listing decks and creating/updating them from other agents or scripts
- 🧩 Viewer route renders decks inside a sandboxed iframe; raw HTML is never injected into the admin UI
- 🔗 Optional additional-resources link on each deck for handouts, docs, forms, or follow-up material
- 🖨️ On-demand PDF downloads with automatic slide pagination and caching
- 📦 SQLite persistence via Prisma with size limits, validation, and automatic migrations
- Next.js 15 (App Router, Server Actions, Turbopack)
- TypeScript & Tailwind CSS 4 preview
- Prisma ORM targeting SQLite
- Sonner for toast notifications
macOS instructions for a clean machine:
- Install Homebrew (skip if already installed):
Then follow the on-screen instructions to add Homebrew to your shell profile (usually
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile && eval "$(/opt/homebrew/bin/brew shellenv)"). - Install Node.js 20 (includes npm):
If prompted, add Node to your PATH (e.g.
brew install node@20
echo 'export PATH="/opt/homebrew/opt/node@20/bin:$PATH"' >> ~/.zshrc). - Verify the tooling:
Both commands should print a version string (for example
node -v npm -v
v20.x.x).
- Clone the repository and change into the directory.
- Copy the example environment file and update the values:
cp .env.example .env
DATABASE_URLstays asfile:./decks.dbeven when you deploy to Turso/LibSQL (the runtime overrides connection details viaLIBSQL_URL).LIBSQL_URL/LIBSQL_AUTH_TOKENcan stay unset locally; production deployments should provide them if you use a remote database.AUTH_SECRETmust be a long random string (e.g.openssl rand -base64 32).ADMIN_EMAILis the single Google account allowed into the admin.ADMIN_PASSWORDis optional and enables password sign-in for that same owner account.ADMIN_API_KEYis optional and enables bearer-token access to the automation API for agents and scripts.GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRETare optional; set both if you want Google OAuth in addition to password login. See “Set up Google OAuth credentials”.NEXTAUTH_URLshould point to the base URL that serves the app (e.g.http://localhost:3000in development,https://your-domain.comin production). Update it per environment.
- Visit https://console.cloud.google.com/ and create (or select) the Google Cloud project that will own your OAuth client.
- In the left navigation, choose Google Auth Platform (or APIs & Services on older projects), open OAuth consent screen, configure the app details, and publish the consent screen so external users can authenticate.
- Under Google Auth Platform → Credentials (or APIs & Services → Credentials), click Create credentials → OAuth client ID.
- Choose Web application, then configure the authorised origins and redirect URIs:
- Authorised JavaScript origins: add each base URL that will host the app. For example:
http://localhost:3000(local dev),https://preview.your-domain.com,https://your-domain.com. - Authorised redirect URIs: add
/api/auth/callback/googlefor every environment. For example:http://localhost:3000/api/auth/callback/google,https://preview.your-domain.com/api/auth/callback/google,https://your-domain.com/api/auth/callback/google.
- Authorised JavaScript origins: add each base URL that will host the app. For example:
- After saving, copy the generated Client ID and Client secret into
.envasGOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET. - Repeat the authorised origins/redirect URIs and environment variables for each environment (staging, preview, production) where the app will run.
- Install dependencies and apply the Prisma migrations:
npm install npx prisma migrate dev
- (Optional) Seed sample deck templates manually:
Each new Google account that signs in receives a private copy of these templates the first time they reach the dashboard.
npm run db:seed
- Start the dev server:
The app is now accessible at http://localhost:3000.
npm run dev
- Visit http://localhost:3000/admin/login and sign in with the configured owner Google account or the admin password.
- Confirm that the dashboard lists the seeded sample decks copied into your account. Use the Copy link and Copy password buttons to grab both values.
- Open the copied URL in a new tab (or private window), paste the password, and verify the iframe renders the seeded deck.
- From the deck detail screen:
- Update the HTML textarea (e.g., modify the heading), click Save changes, then refresh the share link tab to confirm the content updates instantly.
- Click Regenerate link and ensure the old URL now shows “Deck unavailable” while the new link works.
- Toggle Deactivate and confirm the share link returns the “Deck unavailable” message until reactivated.
- Click Generate password then Copy password, reload the share page, and confirm only the new password unlocks the deck.
- Use Delete deck and confirm you are redirected to the dashboard and the share link no longer works.
- Add an Additional resources link, save, and confirm the share page renders the “Additional resources” button that opens the URL in a new tab.
- Unlock the share page, click Download deck, and verify the PDF puts each slide on its own page. Repeat the download to confirm the cached copy returns instantly.
Repeat npm run db:seed at any time to update the reusable sample templates. Existing users keep their copies; new logins receive the latest template content.
- Downloads run headless Chromium in a serverless-friendly mode. Local development uses the full
puppeteerpackage, while production (e.g., Vercel) relies on@sparticuz/chromium+puppeteer-coreto keep the binary slim. - The generator scans for common slide wrappers such as
.slide,.page,[data-slide],[data-page],.slides > *, or plainsectionelements. If nothing obvious matches the script falls back to printing the top-level layout so every deck still renders. - Each successful render stores the PDF bytes, selected selector, slide count, and render time in
DeckPdfCache. Subsequent downloads return the cached copy until the deck HTML changes or the deck is deleted. - Expect the first render to take 1–5 seconds depending on slide complexity. Cached downloads typically finish in under half a second.
- The helper honours optional environment overrides:
CHROMIUM_EXECUTABLE_PATH– point to a custom Chromium build.CHROMIUM_ARGS– append extra launch flags (space-delimited).
- When deploying to Vercel, ensure the function has enough memory (≥1024 MB recommended) and that system packages for fonts/NSS are available.
@sparticuz/chromiumexposes sensible defaults; no additional config is required for local development beyondnpm install.
- Use
FORCE_PDF_FAIL_ONCE=1 npm run dev(or invoke the API route with the env set) to simulate a transient Chromium failure. The route retries once before surfacing a 500, mirroring the viewer’s two-attempt client logic. - Because cached PDFs serve immediately, repeated download attempts are inexpensive. We intentionally rely on caching + the single retry as the guardrail instead of adding per-token rate limiting.
- If your exported HTML uses custom slide wrappers, ensure they map to one of the selectors above or wrap each slide in a dedicated element (e.g.,
<section data-slide>). The admin form includes helper copy with these guidelines. - You can confirm which selector was used by checking server logs (
[pdf] generated deck=... selector=...).
- Navigate to
/admin/loginand sign in with the configured owner Google account or the admin password. - Create a new deck by pasting exported HTML from Claude, Codex, your own app, or any other HTML-producing slide workflow.
- If you use a house HTML slide template, keep reusing it and tell your AI tool to generate each new deck inside that template.
- (Optional) Provide an Additional resources URL to expose a helper button on the share page.
- Use Generate password (or provide your own), then copy both the share link (
/share/<token>) and password for your clients. - Use the saved-deck preview inside admin to check the published version without leaving the editor.
- Edit decks at any time—existing links immediately serve the updated HTML.
- Use Regenerate link to invalidate a previously shared URL, or Deactivate to temporarily block access (viewers will see a “Deck unavailable” message).
- Build one good HTML deck template with the structure, styling, and interactions you want.
- Ask Claude, Codex, or another agent to generate a new deck inside that template.
- Paste the resulting HTML into Deck Manager.
- Add a password if needed and attach an Additional resources link for docs, files, forms, or handoffs.
- Share the published URL.
- Post-client follow-ups with a polished clickable deck, a password-protected share link, and attached resources.
- AI-generated presentations that go through an edit loop, publish to your website, and then get sent out automatically.
- Internal proposals, sales follow-ups, handoff docs, and async presentations that need more interaction than a static PDF.
If you want another agent to publish decks without clicking through the UI, set ADMIN_API_KEY and use the admin API with a bearer token:
Authorization: Bearer <ADMIN_API_KEY>The most useful endpoint for automation is the slug-based upsert route. It lets an agent keep updating the same deck while preserving the existing share link and password by default.
curl -X PUT "http://localhost:3000/api/admin/decks/by-slug/acme-follow-up" \
-H "Authorization: Bearer $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Acme Follow-Up",
"html": "<!DOCTYPE html><html><body><section class=\"slide\">Hello</section></body></html>",
"description": "Post-meeting recap deck",
"generatePassword": true,
"additionalResourceUrl": "https://example.com/resources"
}'Response fields include:
deck.shareUrlfor the client-facing linkdeck.passwordfor the viewer passworddeck.adminUrlfor jumping back into the editoroperationset tocreatedorupdated
GET /api/admin/deckslists workspace decks.GET /api/admin/decks/:idreturns one deck with its HTML.PATCH /api/admin/decks/:idupdates an existing deck by ID.DELETE /api/admin/decks/:iddeletes a deck by ID.PUT /api/admin/decks/by-slug/:slugcreates or updates a deck using a stable automation slug.
- Use a stable slug when you want repeated AI runs to keep updating the same deck.
- Omit
passwordon slug-based updates to keep the current password unchanged. - Set
generatePassword: truewhen you want the server to mint a new password for you. - Set
regenerateShareLink: trueon updates only when you want to rotate the public URL.
Step-by-step instructions (including provisioning Turso/LibSQL and configuring Vercel) live in docs/deployment.md.
npm run dev– local dev server with Turbopacknpm run lint– run ESLint across the projectnpm run build– production buildnpx prisma studio– inspect the SQLite database contents
- Start the UI in your terminal (launches at http://localhost:5555):
npx prisma studio
- Stop it with
Ctrl+Cin the same terminal. If the shell session is gone, find the lingering process and terminate it:Replacelsof -i :5555 kill <PID>
<PID>with the value from thenoderow in thelsofoutput.
- Unable to sign in: Confirm
AUTH_SECRET,ADMIN_EMAIL, and at least one login method are configured. If using Google, ensureGOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETare set and the redirect URI matches/api/auth/callback/google. If using password login, confirmADMIN_PASSWORDmatches what you entered. - Deck link 404: Ensure the deck is marked “Active” in the admin UI and that you copied the latest share link after regeneration.
- Download fails or is slow: Make sure the deck unlocks first, then retry. Check server logs for Chromium launch errors and confirm external assets allow headless access. The second attempt should use the cached PDF.
- Need to simulate a flaky render: Set
FORCE_PDF_FAIL_ONCE=1(in non-production environments) before calling the PDF route. The first attempt will throw, the built-in retry will immediately follow, and logs capture both attempts.
- Deck HTML is arbitrary: users paste fully dynamic pages (custom CSS, JS, animations, Chart.js, etc.), so we need a real browser runtime. Puppeteer with headless Chromium gives us the exact layout/JS execution a deck expects.
- Slide pagination is bespoke: the generator inspects the DOM to find slide-like nodes, clones them, enforces page breaks, and normalises styles—behaviour generic HTML→PDF tools don’t provide.
- We inject print overrides: animations are disabled, gradients preserved, and canvases are snapshotted before printing to keep charts/images intact; owning the pipeline lets us wire those hooks in.
- Caching/retries and metrics are first-class: the renderer plugs directly into
DeckPdfCache, emits structured logs, and obeys our retry semantics—important for consistent behaviour in share downloads. - Deployment parity: running Chromium ourselves keeps local dev, Vercel, and future serverless targets aligned without relying on third-party PDF services or external network calls.
- Large HTML uploads: Decks larger than 2 MB are rejected. Compress assets within the exporting tool if needed.
Happy sharing!