Typescript Best Practices
bretzel-app/crumbs
Provides TypeScript patterns for type-first development, making illegal states unrepresentable, exhaustive handling, and runtime validation.
Guided walkthrough for forking and deploying your own SimplePDF Copilot: hosting choice, Pro-account confirmation, AI-provider wiring, demo customization, deploy, and the SimplePDF whitelist step.
$ npx skills add SimplePDF/simplepdf-embed --skill fork-and-go -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install SimplePDF/simplepdf-embed fork-and-go --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/SimplePDF/simplepdf-embed.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/fork-and-go .claude/skills/fork-and-go && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "fork-and-go" agent skill from https://github.com/SimplePDF/simplepdf-embed/tree/main/skills/fork-and-go into .claude/skills/fork-and-go/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "fork-and-go", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/SimplePDF/simplepdf-embed/tree/main/skills/fork-and-goType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add SimplePDF/simplepdf-embed --skill fork-and-go -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install SimplePDF/simplepdf-embed fork-and-go --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/SimplePDF/simplepdf-embed.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/fork-and-go .agents/skills/fork-and-go && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "fork-and-go" agent skill from https://github.com/SimplePDF/simplepdf-embed/tree/main/skills/fork-and-go into .agents/skills/fork-and-go/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "fork-and-go", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add SimplePDF/simplepdf-embed --skill fork-and-go -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install SimplePDF/simplepdf-embed fork-and-go --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/SimplePDF/simplepdf-embed.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/fork-and-go .cursor/skills/fork-and-go && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "fork-and-go" agent skill from https://github.com/SimplePDF/simplepdf-embed/tree/main/skills/fork-and-go into .cursor/skills/fork-and-go/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "fork-and-go", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/SimplePDF/simplepdf-embed.git --path skills/fork-and-go--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add SimplePDF/simplepdf-embed --skill fork-and-go -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install SimplePDF/simplepdf-embed fork-and-go --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/SimplePDF/simplepdf-embed.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/fork-and-go .gemini/skills/fork-and-go && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "fork-and-go" agent skill from https://github.com/SimplePDF/simplepdf-embed/tree/main/skills/fork-and-go into .gemini/skills/fork-and-go/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "fork-and-go", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install SimplePDF/simplepdf-embed fork-and-goInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add SimplePDF/simplepdf-embed --skill fork-and-go -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/SimplePDF/simplepdf-embed.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/fork-and-go .github/skills/fork-and-go && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "fork-and-go" agent skill from https://github.com/SimplePDF/simplepdf-embed/tree/main/skills/fork-and-go into .github/skills/fork-and-go/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "fork-and-go", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add SimplePDF/simplepdf-embed --skill fork-and-go -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install SimplePDF/simplepdf-embed fork-and-go --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/SimplePDF/simplepdf-embed.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/fork-and-go .opencode/skills/fork-and-go && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "fork-and-go" agent skill from https://github.com/SimplePDF/simplepdf-embed/tree/main/skills/fork-and-go into .opencode/skills/fork-and-go/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "fork-and-go", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
fork-and-goGuided walkthrough for forking and deploying your own SimplePDF Copilot: hosting choice, Pro-account confirmation, AI-provider wiring, demo customization, deploy, and the SimplePDF whitelist step.
Fork And Go is an agent skill from SimplePDF/simplepdf-embed. Guided walkthrough for forking and deploying your own SimplePDF Copilot: hosting choice, Pro-account confirmation, AI-provider wiring, demo customization, deploy, and the SimplePDF whitelist step. Use when a developer wants to fork, self-host, ship, or deploy SimplePDF Copilot.
Its SKILL.md is about 7.9k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Frontend & Design, covering React components. It works with React, JavaScript and TypeScript. The repository describes itself as: PDF editor in the browser – add text, checkboxes, pictures, signatures to PDF files. Merge, rotate PDF pages – iframe, script and React component. The licence is MIT.
9 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 51f5427. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
npmjustnpxvercelnodeghwrangleropensslpythonFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
cloud.digitalocean.comAlso links to:
simplepdf.comdevelopers.cloudflare.comFrom URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
DEMO_CHAT_API_KEYDEMO_STT_OPENAI_API_KEYAI_API_KEYFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Fork And Go loads about 7.9k tokens when it runs. Until then it costs about 73 tokens; SKILL.md has 3,853 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check noted patterns worth knowing about, such as sudo or a known installer.
cp .env.example .envThen edit `.env`:Wait for confirmation that `.env` is filled in.DEMO_CHAT_MODEL=anthropic_haiku_4_5` in `.env`. No code change needed.`DEMO_STT_OPENAI_API_KEY` vars unset in `.env` so the deployment stays out of demo mode. Visitors will see the Model PicAutomated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from SimplePDF/simplepdf-embed at commit 51f5427, republished under its MIT licence (© SimplePDF). 3,853 words, ~7,859 tokens.
.claude/skills/fork-and-go/SKILL.md (or your agent's skills folder).A guided walkthrough for SimplePDF Pro customers forking and deploying their own SimplePDF Copilot.
Walk a developer through forking the SimplePDF Copilot reference implementation into their own product. Covers hosting choice, Pro-account confirmation, AI-provider wiring, demo customization, deploy, and the SimplePDF whitelist step. End state: a running SimplePDF Copilot at their chosen URL, talking to their AI provider, whitelisted on their account.
To get the /fork-and-go slash command in Claude Code, copy this folder into the project's .claude/skills/ — or just point the agent at this file.
Invoke when the user types /fork-and-go or any natural-language equivalent:
Before any question, greet the user in one or two friendly sentences:
Example shape: "Let me help you get SimplePDF Copilot running in your setup. I'll ask a few quick questions to figure out the right path, then we'll wire it together step by step."
After the greeting, ask the FIRST question (Q1 below).
This is the single most important rule in this entire skill. Each of your replies MUST contain at most ONE question. Then STOP and wait for the user to answer.
If a section asks more than one thing, ask the FIRST one only and remember the rest for your next turn.
Forbidden patterns:
The ONLY exception: a clarifying restatement of the SAME question (e.g. "Local only: meaning just npm run dev on your dev machine: or hosted somewhere?"). That's one question with a definition, not two questions.
If you catch yourself drafting more than one question, delete everything after the first one. Do not soften with "and one more thing" or "while you're at it".
When asking the user to pick between known options, use the AskUserQuestion tool whenever it is available, never a plain text list. Free-text answers (e.g. "what's your companyIdentifier?") use a regular question. If AskUserQuestion is not available, ask the same single question as concise plain text.
AskUserQuestion header chips: keep them under 12 chars. Examples: Host, Plan, Provider, Customize.
When recommending a default option, mark it with (Recommended) in the label and put it first.
pnpm in their first reply).The goal: feel like a calm, focused colleague, not a manual.
Use AskUserQuestion:
HostLocal only (Recommended): "Just npm run dev on your dev machine, served at http://localhost:3001. The demo's SimplePDF workspace whitelists that exact origin, so no Pro account is needed. The port has to stay 3001."DigitalOcean App Platform: "One-click deploy via the bundled .do/deploy.template.yaml. Cheapest hosted option (~$12-24/mo)."Cloudflare Containers: "GA since April 2026. Workers Paid plan ($5/mo) required. The copilot Node + nitro stack runs as-is in a Linux container. Needs a small Dockerfile and a wrangler deploy."Other (Vercel / Render / fly.io / Docker / my own server): "The nitro node-server stack works on any PaaS or your own box; we'll set up env vars + build commands, or you run the production build (npm start) wherever you want."DO NOT proceed until they answer.
After Q1, use AskUserQuestion:
PlanYes, I have Pro or higher: "Great, we'll wire it up with your companyIdentifier next."No, but I'll get one: "I'll point you at the sign-up flow next, with one tip about which welcome path to pick."Just exploring: "Local-only is fine without a Pro account (the demo workspace whitelists localhost:3001). Hosted deploy is gated on Pro."If they pick No, but I'll get one, send them this exact guidance and pause until they confirm:
"Sign up at https://simplepdf.com/auth/signup. The welcome flow will ask whether you want to embed SimplePDF in your app or collect submissions. Pick 'collect submissions': that path is short and gets you straight to plan selection, which is what you actually need here. The 'embed in my app' welcome takes you through an integration walkthrough (React / iframe / WordPress / etc.) that you don't need for fork-and-go, since you're already wiring up the embed yourself via this skill. After completing the short onboarding, choose the Pro plan (or higher). Your
companyIdentifieris visible in the dashboard sidebar, right under your company name (a small monospaced chip)."
If Just exploring: set the expectation clearly that local-only works but hosted requires Pro, then proceed (use the demo's spdf-copilot companyIdentifier as a placeholder).
If they have or will have Pro (or higher), ask in plain text (no AskUserQuestion):
"What's your SimplePDF companyIdentifier? It's the subdomain piece of <companyIdentifier>.simplepdf.com. Open your SimplePDF dashboard and look at the sidebar: the identifier is the small chip right under your company name."
If Just exploring, skip this and use spdf-copilot as the placeholder; remind them once near deploy time.
Use AskUserQuestion:
ProviderAnthropic Claude (Recommended): "Ships wired in the demo registry (anthropic_haiku_4_5). Mature tool-calling, broad ecosystem support, predictable pricing."OpenAI: "GPT-4 / GPT-5 family. Solid alternative — needs a small wiring change (Step 5) before demo mode can use it."DeepSeek: "In our testing, on par with Anthropic Claude Haiku 4.5 for the form-filling task, at a meaningfully lower cost per turn. Ships wired (deepseek_v4_flash)."BYOK / custom OpenAI-compatible endpoint: "No server-side provider: visitors paste their own key in the in-app Model Picker. Also covers local/self-hosted OpenAI-compatible endpoints (Ollama, LM Studio, vLLM) via the browser-direct BYOK path — your server isn't in the loop. Lowest ops surface."ONLY ask if Q4 was a named provider (Anthropic / OpenAI / DeepSeek) — the BYOK / custom-endpoint path has no server-side demo config, and for OpenAI note that the Step 5 wiring must land before demo mode works. Use AskUserQuestion:
Demo modeYes, enable demo mode: "Set DEMO_CHAT_API_KEY + DEMO_CHAT_MODEL + DEMO_RATE_LIMIT_TURNS and DEMO_STT_OPENAI_API_KEY. Demo mode is on whenever all four are set: every visitor uses the demo on your keys (no invite links), and the per-IP turn cap bounds cost. Voice + chat share the same demo entitlement, so both keys are required."No, BYOK only: "Leave the demo vars unset. Every visitor brings their own key in the Model Picker. No server-side LLM cost from your account."Use AskUserQuestion:
CustomizeKeep everything (Recommended): "BYOK Model Picker, sample forms, welcome splash, info modal, all of it. Easiest to start; trim later once you know what you want."Strip the demo: "Delete the demo trees (welcome modal, info modal, download modal upsell, social-share, sample forms, demo gating, misbehavior detector). Three folder deletes + one file swap, then small edits in the nine retained files that imported them. You keep the chat surface, BYOK Model Picker, model registry, iframe bridge, locale system."Custom: walk me through each: "We'll go through each demo feature one at a time."Demo code is grouped under demo/ directories specifically so the strip is mechanical:
src/components/demo/ — welcome modal, info modal, download modal, social sharesrc/components/easter-eggs/ — Cerfa d'Or French easter eggsrc/lib/demo/ — sample-form catalogue, demo model registrysrc/server/demo/ — preflight gate, demo-config resolution, misbehavior detector, loader server fnsAfter Q6, BEFORE writing any code, mention in plain prose (NOT a question):
"One thing to flag now so it's not a surprise later: your account's embed-origins whitelist controls where the SimplePDF iframe loads. While the whitelist is empty, all origins are allowed — adding the first origin activates the allow-list and blocks everything else. The demo workspace whitelists
localhost:3001, so local dev works out of the box. For your own deploy URL (e.g.https://my-app.example.com), plan to whitelist it so your account isn't left open to any origin. I'll remind you again at deploy time."
If they picked Local only in Q1 AND are staying on the demo spdf-copilot identifier, skip this entirely (the demo workspace already covers them). A Local-only user on their OWN identifier still needs http://localhost:3001 whitelisted once their allow-list has entries — keep the reminder for them.
This is informational. Do NOT pause for an answer; continue to the wiring sequence.
After all the questions are answered, walk through these steps. ONE step per turn. Pause after each for the user to confirm before moving to the next.
If the user already has the copilot directory open (their cwd looks like …/copilot/), skip to Step 2. Otherwise:
gh repo fork SimplePDF/simplepdf-embed --clone
cd simplepdf-embed/copilotFork, don't just clone — every hosted deploy target in Step 7 builds from a GitHub repo, so the user's Step 5/6 changes must live in a repo they can push to. Without gh: fork in the GitHub UI first, then clone the fork. (Local only explorers who will never deploy can plain-clone upstream.)
Wait for them to confirm they're inside the folder.
npm installIf they prefer pnpm or yarn, that works too: but the bundled package-lock.json is npm-style so the first run will rebuild the lockfile. Note this and let them choose.
cp .env.example .envThen edit .env:
VITE_SIMPLEPDF_COMPANY_IDENTIFIER=<their value from Q3>. If they're Just exploring, leave it as spdf-copilot. Heads-up: any identifier other than spdf-copilot switches the app to customer mode (MODE = 'simplepdf_customer'), which among other things swaps the toolbar's Download button for Submit (submissions flow to their SimplePDF account). This is separate from the env-driven shared-key demo chat (the DEMO_* vars from Q5), which works in either mode.Yes, enable demo mode in Q5, set all four demo vars per .env.example: DEMO_CHAT_API_KEY, DEMO_CHAT_MODEL (anthropic_haiku_4_5 or deepseek_v4_flash), DEMO_RATE_LIMIT_TURNS (per-IP turn cap), and DEMO_STT_OPENAI_API_KEY. Demo mode turns on only when all four are present.REDIS_URL (any Redis-compatible URL: DO Managed Caching for Valkey works) and IP_HASH_SALT (generate with openssl rand -hex 32). Required pair when REDIS_URL is set; the server refuses to boot otherwise.Wait for confirmation that .env is filled in.
npm run devOpen http://localhost:3001. Expected:
The dev script pins port 3001 deliberately. The SimplePDF workspace tied to the companyIdentifier whitelists exactly the origin http://localhost:3001 and only that origin: any other port (3000, 5173) or any other host gets a blocked-origin error screen naming the offending origin (with a link to the embed settings). Don't override the port with --port flags.
If the iframe fails to load:
npm run dev without overrides.companyIdentifier is set to a value whose account whitelists other origins but not localhost:3001 (an account with an empty whitelist allows all origins). The demo's spdf-copilot identifier covers it. Their own Pro identifier requires them to whitelist http://localhost:3001 themselves once their allow-list has entries. Easiest path: just load http://localhost:3001 in the browser once (the embed won't work yet, but the editor records the attempted origin), then open https://<companyIdentifier>.simplepdf.com/account/embed, scroll to Security, and the auto-detected origin will be there ready to one-click approve. Refresh the local page; the iframe now loads.Wait for them to confirm the editor renders.
Open src/server/language_model.ts. The current dispatch handles Anthropic and DeepSeek by name. Per their Q4 choice:
DEMO_CHAT_API_KEY (Anthropic key) + DEMO_CHAT_MODEL=anthropic_haiku_4_5 in .env. No code change needed.DEMO_CHAT_API_KEY (OpenAI key). Then wire the provider in src/lib/demo/demo_model.ts: add the new key to the DemoModel union AND the DemoModelSchema enum (the env value is validated against it — an unknown key silently drops the deployment to BYOK-only), plus its DEMO_MODELS entry. Add an openai case to src/server/language_model.ts (@ai-sdk/openai is already installed; the satisfies never guard fails the build until the case exists). Finally set DEMO_CHAT_MODEL to the new key.DEMO_CHAT_API_KEY (DeepSeek key) + DEMO_CHAT_MODEL=deepseek_v4_flash. Already wired.src/lib/byok/ already supports any OpenAI-compatible endpoint. Defaults are in src/lib/byok/providers.ts (Ollama URL + a default model name). Update if you want different defaults.DEMO_CHAT_* + DEMO_STT_OPENAI_API_KEY vars unset in .env so the deployment stays out of demo mode. Visitors will see the Model Picker on first load.After the wiring, restart npm run dev and send a chat message. Expected: the AI responds, and any tool calls (focus a field, set a value) reflect in the editor.
Wait for them to confirm.
If Keep everything in Q6, skip this step entirely.
If Strip the demo, walk the user through these mechanical steps. They run in order; each one is a single command or a single small edit. Pause after each so the user can run it.
6a — Delete the demo folders
rm -rf src/components/demo src/components/easter-eggs src/server/demo
rm src/lib/demo/forms.tsThat removes: welcome modal, info modal, download modal (with the Pro upsell), social-share component, Cerfa d'Or easter egg, sample-form catalogue, demo-config resolver, misbehavior detector, preflight gate, demo-only loader server fns.
Keep src/lib/demo/demo_model.ts — despite the folder name it is the model registry, not demo-only: the retained chat path imports it (chat_pane.tsx, model_picker_modal.tsx, api/chat.ts, server/language_model.ts).
The deletes leave compile errors at these retained files; steps 6b-6g clear them one by one: routes/index.tsx (WelcomeModal, forms, loader helpers), routes/api/chat.ts / api/summarize.ts / api/transcribe.ts (preflight gate), routes/api/transcribe.test.ts (misbehavior import — delete that test file or its import), components/layout.tsx (InfoModal, CerfaDorModal), components/error_banner.tsx (SocialShare), components/chat/chat_pane.tsx (DownloadModal), plus the forms catalogue consumers (form_picker.tsx, chat_pane.tsx).
6b — Replace the sample-form catalogue
SimplePDF Copilot needs a single PDF URL to load on first paint. Recreate src/lib/demo/forms.ts (same path, so no import edits anywhere) with the customer's own:
export type FormId = 'default'
export type FormConfig = {
id: FormId
useCaseKey: string
subtitleKey?: string
labelKey: string
pdfUrl: string
}
export type LocaleForms = {
order: FormId[]
forms: Record<FormId, FormConfig>
}
const FORM: FormConfig = {
id: 'default',
pdfUrl: 'https://your-cdn.example.com/your-form.pdf',
// labelKey + useCaseKey are i18n keys; point them at strings you keep,
// or hardcode short labels.
labelKey: 'forms.labels.default',
useCaseKey: 'forms.useCases.default',
}
export const DEFAULT_FORM_ID: FormId = 'default'
export const getDefaultFormIdForLocale = (_locale: string): FormId => 'default'
export const isFormId = (value: unknown): value is FormId => value === 'default'
export const getFormsForLocale = (_locale: string): LocaleForms => ({
forms: { default: FORM },
order: ['default'],
})Two follow-up edits — 'custom' (the demo's upload-your-own flow) is gone from the union, so both comparisons stop compiling:
routes/index.tsx: replace the requiresUserUpload = url === undefined && form === 'custom' line with const requiresUserUpload = false (or keep a 'custom' member if you want the native-file-picker flow).components/form_picker.tsx: the subtitle ternary form.id === 'custom' ? t('forms.customSubtitle') : … — drop the ternary, keeping t(form.subtitleKey ?? form.useCaseKey).Or wire a runtime loader (your own storage) — but the static one is fine for most forks.
6c — Replace the demo gates with a single static resolution
Three callers (src/routes/api/chat.ts, src/routes/api/summarize.ts, and src/routes/api/transcribe.ts) use applyDemoPreflight from the now-deleted src/server/demo/gate.ts. Replace the import + call with a static resolution that reads your API key from env (transcribe also reads DEMO_STT_OPENAI_API_KEY directly, so it keeps working once the preflight is replaced):
// at the top of chat.ts / summarize.ts / transcribe.ts, replace the demo import with
// (transcribe.ts only consumes bucket + lifetime from the resolution):
import { hashIp, getClientIp, isSameOrigin, looksLikeBrowserFetch } from '../../server/rate_limit'
import type { DemoModel } from '../../lib/demo/demo_model'
// inside the POST handler, replace the preflight block with:
const ip = getClientIp(request)
const ipHash = await hashIp(ip)
const resolution: { apiKey: string; bucket: string; lifetime: number; model: DemoModel } = {
apiKey: process.env.AI_API_KEY ?? '',
// The rate-limit bucket name is per-customer convention; "global"
// collapses every IP into one bucket. Use whatever you want.
bucket: 'global',
lifetime: 1000, // very high cap; tighten if you want IP-rate-limiting
model: 'anthropic_haiku_4_5', // a DEMO_MODELS handle from demo_model.ts, NOT a provider model id
}What this removes: applyDemoPreflight was also a gate — it rejected cross-origin non-browser calls (isSameOrigin / looksLikeBrowserFetch, both retained in src/server/rate_limit.ts), blocked flagged IPs, and 503'd when unconfigured. With a server-side AI_API_KEY here, dropping all of that ships an open cross-origin LLM proxy. Keep at least the origin check at the top of the handler:
if (!isSameOrigin(request) && !looksLikeBrowserFetch(request)) {
return new Response(null, { status: 403 })
}If you don't want any IP-rate-limiting at all, you can also delete the whole rate-limit section in chat.ts (the rateLimiter.isReady() guard AND the rateLimiter.check call) — along with everything only it consumed: the ip/ipHash locals and the hashIp, getClientIp (unless kept for the origin check above), rateLimiter, and RateLimitDecision imports (noUnusedLocals turns any leftover into a build error). The limiter primitive in src/server/rate_limit.ts itself is generic and can stay — without REDIS_URL it falls back to an in-memory per-instance limiter (fine for a single container; multi-instance deploys need Redis).
6d — Drop the demo references in routes/index.tsx
The file imports DemoGate, readDemoGate, readWelcomeDismissed, and writeWelcomeDismissedCookie from src/server/demo/loader_helpers.ts (now deleted). Replace the import block AND the standalone export type { DemoGate } re-export just below it (keeping both would be a duplicate-identifier error) with:
import type { DemoModel } from '../lib/demo/demo_model'
// Keep the full union: retained code (chat_pane, model_picker_modal, voice
// capability resolution) branches on both members with exhaustive switches.
export type DemoGate = { kind: 'byok' } | { kind: 'demo'; model: DemoModel }
const STATIC_DEMO_GATE: DemoGate = { kind: 'byok' }Then in the route's loader, replace the Promise.all([readDemoGate(), readWelcomeDismissed()]) call with:
loader: async () => ({ demoGate: STATIC_DEMO_GATE }),welcomeDismissed disappears from the loader data because everything it fed is going: remove the WelcomeModal import + JSX (<WelcomeModal ... />), the dismissWelcome callback (it calls the deleted writeWelcomeDismissedCookie server fn), and every remaining welcomeDismissed reference.
6e — Drop demo references in src/components/layout.tsx
Layout currently imports the (deleted) InfoModal from ./demo/info_modal and CerfaDorModal from ./easter-eggs/cerfa_dor_modal. Delete both imports + the JSX + the URL-search reading that opens them (?show=info, ?show=cerfa_dor), and the header's info trigger (its header.whatIsThisDemo aria-label key dies in 6g).
With ?show=info gone, the WelcomeBanner's info link in chat_pane.tsx points at nothing — delete that anchor (the chat.welcomeInfoLink line) too.
6f — Drop the social share from the error banner, and the download modal from the chat pane
src/components/error_banner.tsx references SocialShare from ./demo/social_share (deleted). Delete the import and the whole share block (the SocialShare JSX plus the surrounding chat.shareHero copy). Keep RateLimitPanel — the retained chat_pane.tsx renders it on the voice rate-limit path.
src/components/chat/chat_pane.tsx imports DownloadModal from ../demo/download_modal (deleted). Delete the import and its JSX/trigger.
6g — Strip demo-flavoured locale keys
Run a sweep across src/locales/*.json removing these keys (they're now unreferenced):
chat.shareHero, chat.shareCtaLabel, chat.shareCopyLink, chat.shareCopied, chat.shareTweetText
chat.welcomeInfoLink (its ?show=info target died in 6e)
header.whatIsThisDemo (the info trigger died in 6e)
welcomeModal.* (whole tree)
infoModal.* (whole tree EXCEPT infoModal.close — the retained ui/modal.tsx uses it as the close-button aria-label)
download.* (whole tree)
cerfaDor.* (whole tree — exists only in fr.json)
forms.* (whole tree — then add the two keys 6b's template references, forms.labels.default and forms.useCases.default, to each locale you keep, or hardcode the labels instead)Keep chat.welcomeTitle / chat.welcomeBody / chat.welcomeCta and the chat.errorRateLimited* keys — the retained chat pane's BYOK empty state (WelcomeBanner) and rate-limit panel still render them.
Sweep the locale files with a tiny python -c "import json, sys; ..." one-liner per file.
Replace header.brand ("SimplePDF Copilot Demo") with the customer's brand name in en.json + every other locale.
6h — Verify
From inside the copilot/ directory:
npx tsc --noEmit
npm run devOpen http://localhost:3001. Expected: the chat sidebar shows the BYOK Model Picker (or sends straight to your server's chat.ts if you wired a static API key in step 6c), the editor loads your replacement PDF from step 6b, no welcome modal, no info modal trigger, no Cerfa easter egg.
If tsc or runtime fails: the most common cause is a stale import to a deleted file. Run grep -rnE "from '.*(demo|easter-eggs)" src/ — any hit on a DELETED path is something missed in 6b-6g (hits on lib/demo/demo_model and lib/demo/forms are expected: both files still exist).
If Custom: walk me through each: ask them which feature they want to address first (sample forms / info modal / BYOK Model Picker / share-link UI / sample documents). Walk through ONE at a time, pausing after each.
Local only in Q1)Per their Q1 choice:
First, for every hosted target: push the Step 5/6 changes to the user's fork — deploying upstream SimplePDF/simplepdf-embed ships none of their work.
.do/deploy.template.yaml both reference SimplePDF/simplepdf-embed — substitute the user's fork: edit the template's repo: in their fork, then use https://cloud.digitalocean.com/apps/new?repo=https://github.com/<their-owner>/simplepdf-embed/tree/main. DigitalOcean will prompt for VITE_SIMPLEPDF_COMPANY_IDENTIFIER and (optionally) the demo vars (DEMO_CHAT_API_KEY / DEMO_CHAT_MODEL / DEMO_RATE_LIMIT_TURNS / DEMO_STT_OPENAI_API_KEY) / REDIS_URL / IP_HASH_SALT.RUN npm ci && npm run build, CMD ["node", ".output/server/index.mjs"], expose port 3000), reference it from the wrangler config's container settings, then run npx wrangler deploy. See https://developers.cloudflare.com/containers/. Set secrets with npx wrangler secret put DEMO_CHAT_API_KEY (and the other demo vars) etc. Cloudflare's edge sits in front for free WAF + caching.node-server preset works on Vercel's Node runtime. From the copilot folder, run vercel deploy and set the env vars via the dashboard or vercel env add.npm run build, start command npm start, and configure env vars in the host's dashboard. fly.io needs a Dockerfile (build the production output, run node .output/server/index.mjs).npm run build produces .output/. Bundle it in your Dockerfile, expose port 3000, run node .output/server/index.mjs.Wait for them to confirm the deploy succeeded and they have a URL.
Local only)Heads-up: an account with an empty whitelist allows the embed on all origins — whitelisting the first origin is what activates the allow-list (blocking everything else). So on a fresh account the deploy URL may load right away; whitelist it anyway so the account isn't left open to any origin.
The fastest path uses the editor's origin auto-detection:
https://my-app.example.com). If the account already has whitelisted origins, the embed won't work there (the editor loads but stays inert), but the attempted origin is recorded server-side.https://<companyIdentifier>.simplepdf.com/account/embed (replace <companyIdentifier> with their value from Q3).If they prefer to whitelist before opening the deploy URL: the same Whitelist origin modal also accepts a manually-typed origin. Match the protocol (https://) and host without a trailing slash.
Then open the deploy URL. The iframe should load. If not, the most likely causes:
Wait for them to confirm the iframe loads.
Walk through one full chat turn on the deployed URL:
Once that's confirmed, you're done.
Wrap with: "You're set. SimplePDF Copilot is running on your domain, talking to your AI provider, whitelisted on your account. The README at copilot/README.md has more on customization. Reach engineering@simplepdf.com if you hit anything weird."
Do NOT add a recap, a checklist, or a "what's next" section unless the user asks.
If the user asks anything outside the scope of this fork-and-go journey (pricing, plan comparison, embedding the editor without SimplePDF Copilot, debugging an unrelated SimplePDF feature), point them at:
© SimplePDF, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in skills/fork-and-go of SimplePDF/simplepdf-embed.
Open the folder on GitHubat commit 51f5427
Fork And Go next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Fork And Go this skillSimplePDF/simplepdf-embed | 407 | — | ~7.9k | Automated safety check: Notes | MIT | |
| Typescript Best Practicesbretzel-app/crumbs | 127 | 1 repos | ~2.2k | Automated safety check: Pass | MIT | |
| Connect Component To Figmadequelabs/cauldron | 129 | — | ~2k | Automated safety check: Pass | MPL-2.0 | |
| Javascript Practiceseser/stack | 128 | — | ~599 | Automated safety check: Pass | Custom licence | |
| Typescript Rulessoftspark/ai-toolkit | 179 | — | ~2.7k | Automated safety check: Notes | Apache-2.0 | |
| South Admin CRUD Generatorsouthliu/south-admin-react | 580 | — | ~1.7k | Automated safety check: Pass | MIT |
bretzel-app/crumbs
Provides TypeScript patterns for type-first development, making illegal states unrepresentable, exhaustive handling, and runtime validation.
dequelabs/cauldron
Add a Figma Code Connect (.figma.tsx) file for a Cauldron React component.
eser/stack
TS and JS conventions for eserstack packages: namespace imports, mod.ts entries, cross-runtime APIs, explicit checks, async, tests and laroux React components.
softspark/ai-toolkit
TypeScript/JavaScript coding rules: style, patterns, security, testing.
southliu/south-admin-react
Generates a full CRUD page - page component, data model and API client - from the south-admin-react project's own VS Code snippet templates.
HorusGoul/eslint-plugin-react-render-types
Composition patterns for building React components with @renders type annotations from eslint-plugin-react-render-types.
SimplePDF/simplepdf-embed
Edit and fill PDF documents. An agent skill from SimplePDF/simplepdf-embed.
SimplePDF/simplepdf-embed
Integrate SimplePDF into a web application for PDF viewing, editing, filling, signing, programmatic control, AI-agent interaction, human-in-the-loop form prefilling, submissions, webhooks, or…
Works with
Categories
Guided walkthrough for forking and deploying your own SimplePDF Copilot: hosting choice, Pro-account confirmation, AI-provider wiring, demo customization, deploy, and the SimplePDF whitelist step. Fork And Go is an agent skill from SimplePDF/simplepdf-embed. Guided walkthrough for forking and deploying your own SimplePDF Copilot: hosting choice, Pro-account confirmation, AI-provider wiring, demo customization, deploy, and the SimplePDF whitelist step.
Fork And Go fits situations like: A developer wants to fork; deploy SimplePDF Copilot.
Run `npx skills add SimplePDF/simplepdf-embed --skill fork-and-go -a claude-code`. Or copy the skill folder (skills/fork-and-go in SimplePDF/simplepdf-embed) into .claude/skills/fork-and-go in your project. Claude Code loads it when a task matches its description.
Run `npx skills add SimplePDF/simplepdf-embed --skill fork-and-go -a codex`. Or copy the skill folder (skills/fork-and-go in SimplePDF/simplepdf-embed) into .agents/skills/fork-and-go in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add SimplePDF/simplepdf-embed --skill fork-and-go -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/fork-and-go, .gemini/skills/fork-and-go, .github/skills/fork-and-go and .opencode/skills/fork-and-go in your project.
Going by SKILL.md and its folder, Fork And Go needs the command-line tools its instructions call (npm, just, npx, vercel, node and gh) and credentials named DEMO_CHAT_API_KEY, DEMO_STT_OPENAI_API_KEY and AI_API_KEY. Our summary lists: Docker; A credential in DEMO_CHAT_API_KEY; A credential in DEMO_STT_OPENAI_API_KEY.
SKILL.md names 3 domains. In commands or code: cloud.digitalocean.com; the agent is likely to contact it when it follows the instructions. As links in the text: simplepdf.com and developers.cloudflare.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found notes only (mentions a .env file), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.
Fork And Go is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 7.9k tokens (SKILL.md is roughly 31k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Fork And Go: Typescript Best Practices (bretzel-app/crumbs, 127 stars), Connect Component To Figma (dequelabs/cauldron, 129 stars), Javascript Practices (eser/stack, 128 stars) and Typescript Rules (softspark/ai-toolkit, 179 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
SimplePDF (a GitHub organization) maintains it in SimplePDF/simplepdf-embed, which has 407 GitHub stars. The repository holds 3 skills in this directory. The repository was last updated on September 30, 2026.
Source: SimplePDF/simplepdf-embed on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.