[{"data":1,"prerenderedAt":824},["ShallowReactive",2],{"repo-tree":3,"repo-\u002Finfra\u002Freadme":283},[4,7,10,13,16,19,22,25,28,31,34,37,40,43,46,49,52,55,58,61,64,67,69,72,75,78,81,84,86,88,90,93,96,99,102,105,108,111,114,117,120,123,125,127,129,131,133,135,138,141,143,146,149,152,155,158,161,164,167,169,172,175,178,180,183,186,189,192,195,198,201,203,206,209,212,215,218,221,224,227,230,233,236,239,242,245,248,251,254,257,260,263,266,269,272,275,278,281],{"path":5,"title":6},"\u002Fagents\u002Fbackend-code-style","Backend Conventions",{"path":8,"title":9},"\u002Fagents\u002Fdatabase","Database",{"path":11,"title":12},"\u002Fagents\u002Fportal-code-style","Portal Conventions",{"path":14,"title":15},"\u002Fagents\u002Ftranslation","Translation",{"path":17,"title":18},"\u002Fconventions\u002Fbackend-coding","Backend coding conventions",{"path":20,"title":21},"\u002Fconventions\u002Ffrontend-coding","Frontend coding conventions",{"path":23,"title":24},"\u002Fdevelopment-process","Development process",{"path":26,"title":27},"\u002Flearning-api-preview-hetzner-setup","Learning API Preview on Hetzner + Cloudflare",{"path":29,"title":30},"\u002Flearning-api-preview-vm-plan","Learning API Preview VM Plan",{"path":32,"title":33},"\u002Fmonorepo-structure","Monorepo structure",{"path":35,"title":36},"\u002Foperations","Operations — bugs and support",{"path":38,"title":39},"\u002Fpostmortems\u002F2026-03-16_onboarding-currency-regression","Onboarding Zod transform silently broken — web signups assigned wrong checkout currency",{"path":41,"title":42},"\u002Fpostmortems\u002Freadme","Postmortems",{"path":44,"title":45},"\u002Fpostmortems\u002F_template","TEMPLATE",{"path":47,"title":48},"\u002Fpostmortems\u002Fposthog-comparison","Postmortem practice — comparison with PostHog",{"path":50,"title":51},"\u002Fpreview-environment-plan","Preview Environment Plan",{"path":53,"title":54},"\u002Fprinciples","Engineering principles",{"path":56,"title":57},"\u002Fworking-with-ai","Working with AI",{"path":59,"title":60},"\u002F.claude\u002Fskills\u002Feval-playground\u002Fskill","Eval Playground — Co-development Skill",{"path":62,"title":63},"\u002F.claude\u002Fskills\u002Ffigma-diff-section\u002Fskill","Figma Diff Section Pipeline",{"path":65,"title":66},"\u002Fagents","AGENTS.md",{"path":68,"title":66},"\u002Fclaude",{"path":70,"title":71},"\u002Freadme","Studyflash",{"path":73,"title":74},"\u002Fapps\u002Fcore-api\u002Fagents","Core API (apps\u002Fcore-api)",{"path":76,"title":77},"\u002Fapps\u002Fcore-api\u002Freadme","README",{"path":79,"title":80},"\u002Fapps\u002Femail-previews\u002Fagents","Email Previews (apps\u002Femail-previews)",{"path":82,"title":83},"\u002Fapps\u002Flanding-page\u002Fagents","Landing Page (apps\u002Flanding-page)",{"path":85,"title":83},"\u002Fapps\u002Flanding-page\u002Fclaude",{"path":87,"title":66},"\u002Fapps\u002Flearning-api\u002Fagents",{"path":89,"title":77},"\u002Fapps\u002Flearning-api\u002Freadme",{"path":91,"title":92},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Feval_metrics_design","Surface-Specific Eval Metrics Design",{"path":94,"title":95},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Ftest_set","Quiz Eval Test Set",{"path":97,"title":98},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Ffrontend\u002Freadme","React + TypeScript + Vite",{"path":100,"title":101},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Fknown-issues\u002Fcontent-pillar-shallow-coverage\u002Freadme","Content pillar misses subtopics in dense documents",{"path":103,"title":104},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Fknown-issues\u002Fdocling-empty-section-headers\u002Freadme","Empty section headers dropped by docling chunker",{"path":106,"title":107},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Fknown-issues\u002Fdocling-table-reading-order\u002Freadme","Table\u002Fbox layout causes wrong reading order",{"path":109,"title":110},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Fmetrics\u002Freadme","Quiz eval metrics — canonical rubrics",{"path":112,"title":113},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Freports\u002F2026-04-12-quiz-summary-feedback-current-state","Quiz and Summary Feedback Current State",{"path":115,"title":116},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Freports\u002F2026-04-24-quiz-eval-metrics","Quiz Evaluation Metrics",{"path":118,"title":119},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Freports\u002F2026-05-01-quiz-eval-current-state","Quiz Eval Current State",{"path":121,"title":122},"\u002Fapps\u002Flearning-api\u002Fmonitoring\u002Freadme","Monitoring Stack",{"path":124,"title":77},"\u002Fapps\u002Flearning-api\u002Fshared\u002Freadme",{"path":126,"title":77},"\u002Fapps\u002Flearning-api\u002Fworkers\u002Flearning_agents\u002Fflashcard_agent\u002Freadme",{"path":128,"title":77},"\u002Fapps\u002Flearning-api\u002Fworkers\u002Flearning_agents\u002Fingestion_agent\u002Freadme",{"path":130,"title":77},"\u002Fapps\u002Flearning-api\u002Fworkers\u002Flearning_agents\u002Fquiz_agent\u002Freadme",{"path":132,"title":77},"\u002Fapps\u002Flearning-api\u002Fworkers\u002Flearning_agents\u002Fsummary_agent\u002Freadme",{"path":134,"title":77},"\u002Fapps\u002Flearning-api\u002Fworkers\u002Fparser\u002Freadme",{"path":136,"title":137},"\u002Fapps\u002Fmarketing-emails-preview\u002Fagents","Marketing Emails Preview (apps\u002Fmarketing-emails-preview)",{"path":139,"title":140},"\u002Fapps\u002Fmobile-app\u002Fagents","StudyFlash Mobile App - Claude Code Configuration",{"path":142,"title":140},"\u002Fapps\u002Fmobile-app\u002Fclaude",{"path":144,"title":145},"\u002Fapps\u002Fmountain-max\u002Fagents","Mountain Max (apps\u002Fmountain-max)",{"path":147,"title":148},"\u002Fapps\u002Fmountain-max\u002Fgame\u002Freadme","Mountain Max Game",{"path":150,"title":151},"\u002Fapps\u002Fportal\u002Fagents","Portal (apps\u002Fportal)",{"path":153,"title":154},"\u002Fapps\u002Fportal\u002Freadme","Nuxt Minimal Starter",{"path":156,"title":157},"\u002Fapps\u002Fportal\u002Fapp\u002Fcomposables\u002Ffiles\u002Freadme","File Upload Composables",{"path":159,"title":160},"\u002Fapps\u002Fportal\u002Fdocs\u002Flibrary-routing","Library Routing Documentation",{"path":162,"title":163},"\u002Fapps\u002Fsupabase\u002Fagents","Supabase (apps\u002Fsupabase)",{"path":165,"title":166},"\u002Fapps\u002Fwrapped\u002Fagents","Wrapped (apps\u002Fwrapped)",{"path":168,"title":98},"\u002Fapps\u002Fwrapped\u002Freadme",{"path":170,"title":171},"\u002Finfra\u002Freadme","infra\u002F",{"path":173,"title":174},"\u002Finfra\u002Fdns\u002Freadme","DNS Infrastructure",{"path":176,"title":177},"\u002Finfra\u002Fdokploy\u002Freadme","studyflash-dokploy",{"path":179,"title":77},"\u002Finfra\u002Fdokploy\u002Fsdk\u002Fnodejs\u002Freadme",{"path":181,"title":182},"\u002Finfra\u002Finfisical\u002Freadme","Infisical Infrastructure",{"path":184,"title":185},"\u002Finfra\u002Flearning-api\u002Freadme","Pulumi GCP TypeScript Template",{"path":187,"title":188},"\u002Finfra\u002Fopenreplay\u002Freadme","OpenReplay on Hetzner",{"path":190,"title":191},"\u002Finfra\u002Fscripts\u002Freadme","infra\u002Fscripts\u002F",{"path":193,"title":194},"\u002Finfra\u002Fturborepo-cache\u002Freadme","Turborepo Remote Cache Infrastructure",{"path":196,"title":197},"\u002Finternal\u002Fchatwoot\u002Freadme","Chatwoot Infrastructure",{"path":199,"title":200},"\u002Finternal\u002Fchatwoot\u002Fprovider\u002Freadme","studyflash-chatwoot-provider",{"path":202,"title":77},"\u002Finternal\u002Fchatwoot\u002Fprovider\u002Fsdk\u002Fnodejs\u002Freadme",{"path":204,"title":205},"\u002Finternal\u002Fdocs\u002Freadme","internal\u002Fdocs",{"path":207,"title":208},"\u002Finternal\u002Fsupport-bot\u002Fclaude","Support Bot (Maximilian)",{"path":210,"title":211},"\u002Finternal\u002Fsupport-bot\u002Freadme","Studyflash Customer Support Bot (Maximilian)",{"path":213,"title":214},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Faccount_issues","Account Issues",{"path":216,"title":217},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fbilling_invoice","Billing Invoice",{"path":219,"title":220},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fcontent_upload","Content Upload",{"path":222,"title":223},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fdata_loss","Data Loss",{"path":225,"title":226},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fflashcard_issues","Flashcard Issues",{"path":228,"title":229},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fgarbage","Garbage",{"path":231,"title":232},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fgeneral_how_to","General How To",{"path":234,"title":235},"\u002Finternal\u002Fsupport-bot\u002Fkb","Knowledge Base Index",{"path":237,"title":238},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Flanguage_issues","Language Issues",{"path":240,"title":241},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fmindmap_issues","Mindmap Issues",{"path":243,"title":244},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fmisunderstanding","Misunderstanding",{"path":246,"title":247},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fmock_exam_issues","Mock Exam Issues",{"path":249,"title":250},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fpodcast_issues","Podcast Issues",{"path":252,"title":253},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fquiz_issues","Quiz Issues",{"path":255,"title":256},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Frefund_request","Refund Request",{"path":258,"title":259},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fsubscription_cancellation","Subscription Cancellation",{"path":261,"title":262},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fsubscription_info","Subscription Info",{"path":264,"title":265},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fsummary_issues","Summary Issues",{"path":267,"title":268},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Ftechnical_errors","Technical Errors",{"path":270,"title":271},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fvideo_issues","Video Issues",{"path":273,"title":274},"\u002Fpackages\u002Fcommon\u002Fdocs\u002Fearly-access-features","Declarative Early Access Features",{"path":276,"title":277},"\u002Fpackages\u002Fcommon\u002Fscripts\u002Freadme","Common Package Scripts",{"path":279,"title":280},"\u002Fpackages\u002Fdevtools\u002Ffigma-plugins\u002Freadme","Figma plugins",{"path":282,"title":77},"\u002Fpackages\u002Fpulumi-infisical\u002Freadme",{"id":284,"title":171,"body":285,"description":817,"extension":818,"lastReviewed":819,"meta":820,"navigation":519,"owner":819,"path":170,"seo":821,"status":819,"stem":822,"tags":819,"__hash__":823},"repo\u002Finfra\u002FREADME.md",{"type":286,"value":287,"toc":807},"minimark",[288,295,319,324,481,485,644,662,666,671,678,693,720,738,742,761,765,784,788,803],[289,290,292],"h1",{"id":291},"infra",[293,294,171],"code",{},[296,297,298,299,302,303,306,307,310,311,314,315,318],"p",{},"Pulumi projects for everything we self-host. One project per stack\n(",[293,300,301],{},"infra\u002F\u003Cstack>\u002F","), each with its own ",[293,304,305],{},"Pulumi.yaml",", ",[293,308,309],{},"index.ts",", and\n",[293,312,313],{},"Pulumi.\u003Cenv>.yaml",". Stacks are independent — they share the R2 state\nbackend and the ",[293,316,317],{},"pulumi-state"," GCP KMS key for secret encryption,\nbut otherwise don't depend on each other.",[320,321,323],"h2",{"id":322},"stacks","Stacks",[325,326,327,340],"table",{},[328,329,330],"thead",{},[331,332,333,337],"tr",{},[334,335,336],"th",{},"Folder",[334,338,339],{},"What",[341,342,343,362,381,394,410,423,442,455,468],"tbody",{},[331,344,345,355],{},[346,347,348],"td",{},[349,350,352],"a",{"href":351},".\u002Fdns",[293,353,354],{},"dns\u002F",[346,356,357,358,361],{},"Cloudflare zones, DNS records, and redirect rulesets across all ",[293,359,360],{},"studyflash.*"," domains",[331,363,364,372],{},[346,365,366],{},[349,367,369],{"href":368},".\u002Fgatus",[293,370,371],{},"gatus\u002F",[346,373,374,380],{},[349,375,379],{"href":376,"rel":377},"https:\u002F\u002Fgatus.io",[378],"nofollow","Gatus"," uptime monitoring",[331,382,383,391],{},[346,384,385],{},[349,386,388],{"href":387},".\u002Fgrafana",[293,389,390],{},"grafana\u002F",[346,392,393],{},"Self-hosted Grafana + Prometheus + Pushgateway",[331,395,396,404],{},[346,397,398],{},[349,399,401],{"href":400},".\u002Fkms",[293,402,403],{},"kms\u002F",[346,405,406,407,409],{},"The shared ",[293,408,317],{}," GCP KMS key all other stacks use as their secrets provider",[331,411,412,420],{},[346,413,414],{},[349,415,417],{"href":416},".\u002Flearning-api",[293,418,419],{},"learning-api\u002F",[346,421,422],{},"learning-api hosts: the Hetzner UAT VM, Azure Container Apps for prod, GCP MIGs for parser",[331,424,425,433],{},[346,426,427],{},[349,428,430],{"href":429},".\u002Fopenreplay",[293,431,432],{},"openreplay\u002F",[346,434,435,436,441],{},"Self-hosted ",[349,437,440],{"href":438,"rel":439},"https:\u002F\u002Fopenreplay.com",[378],"OpenReplay"," on a dedicated Hetzner Cloud VM",[331,443,444,452],{},[346,445,446],{},[349,447,449],{"href":448},".\u002Ftriton-inference",[293,450,451],{},"triton-inference\u002F",[346,453,454],{},"Triton Inference Server + vLLM sidecar GPU infra",[331,456,457,465],{},[346,458,459],{},[349,460,462],{"href":461},".\u002Fturborepo-cache",[293,463,464],{},"turborepo-cache\u002F",[346,466,467],{},"Turborepo remote cache backend on GCP",[331,469,470,478],{},[346,471,472],{},[349,473,475],{"href":474},".\u002Fzero-trust",[293,476,477],{},"zero-trust\u002F",[346,479,480],{},"Cloudflare Zero Trust device profiles and WARP split tunnel",[320,482,484],{"id":483},"creating-a-new-stack","Creating a new stack",[486,487,492],"pre",{"className":488,"code":489,"language":490,"meta":491,"style":491},"language-sh shiki shiki-themes github-light github-dark","# 1. From the folder that will own the stack (under infra\u002F for shared\n#    infra, internal\u002F\u003Cservice>\u002F for a service-scoped stack).\ncd infra\u002F\u003Cstack>   # or: cd internal\u002F\u003Cservice>\n\n# 2. Init Pulumi.yaml — runtime: nodejs\u002Fpnpm. Add `main: infra.ts` (or\n#    whatever entry filename) under runtime.options if you want an\n#    entry name other than index.ts. Mirror the description style of\n#    sibling stacks so the stack catalog above stays readable.\n\n# 3. Add the stack itself. ALWAYS pass --secrets-provider pointing at\n#    the shared GCP KMS key — never accept the default passphrase\n#    provider, that diverges from every other stack and trips local\n#    deploys.\nAWS_PROFILE=studyflash-pulumi pnpm exec pulumi stack init prod \\\n  --secrets-provider=\"gcpkms:\u002F\u002Fprojects\u002Fstudyflash-security\u002Flocations\u002Feurope-west6\u002FkeyRings\u002Fpulumi-state\u002FcryptoKeys\u002Fpulumi-state\"\n\n# 4. Set the package.json entry so Pulumi finds your TS:\n#       \"main\": \"infra.ts\"\n#    Pulumi reads package.json's `main` field, NOT Pulumi.yaml's\n#    runtime.options.main. Don't burn an hour on this.\n\n# 5. Wrap apply in `infisical run` so secrets at \u002Finfra\u002F\u003Cstack>\u002F (or\n#    \u002Finternal\u002F\u003Cservice>\u002F) show up as env vars. Add a `pnpm run up`\n#    script in package.json that does the wrapping; do NOT invoke\n#    `pulumi up` directly from CI or local shells.\n","sh","",[293,493,494,502,508,514,521,527,533,539,545,550,556,562,568,574,580,586,591,597,603,609,615,620,626,632,638],{"__ignoreMap":491},[495,496,499],"span",{"class":497,"line":498},"line",1,[495,500,501],{},"# 1. From the folder that will own the stack (under infra\u002F for shared\n",[495,503,505],{"class":497,"line":504},2,[495,506,507],{},"#    infra, internal\u002F\u003Cservice>\u002F for a service-scoped stack).\n",[495,509,511],{"class":497,"line":510},3,[495,512,513],{},"cd infra\u002F\u003Cstack>   # or: cd internal\u002F\u003Cservice>\n",[495,515,517],{"class":497,"line":516},4,[495,518,520],{"emptyLinePlaceholder":519},true,"\n",[495,522,524],{"class":497,"line":523},5,[495,525,526],{},"# 2. Init Pulumi.yaml — runtime: nodejs\u002Fpnpm. Add `main: infra.ts` (or\n",[495,528,530],{"class":497,"line":529},6,[495,531,532],{},"#    whatever entry filename) under runtime.options if you want an\n",[495,534,536],{"class":497,"line":535},7,[495,537,538],{},"#    entry name other than index.ts. Mirror the description style of\n",[495,540,542],{"class":497,"line":541},8,[495,543,544],{},"#    sibling stacks so the stack catalog above stays readable.\n",[495,546,548],{"class":497,"line":547},9,[495,549,520],{"emptyLinePlaceholder":519},[495,551,553],{"class":497,"line":552},10,[495,554,555],{},"# 3. Add the stack itself. ALWAYS pass --secrets-provider pointing at\n",[495,557,559],{"class":497,"line":558},11,[495,560,561],{},"#    the shared GCP KMS key — never accept the default passphrase\n",[495,563,565],{"class":497,"line":564},12,[495,566,567],{},"#    provider, that diverges from every other stack and trips local\n",[495,569,571],{"class":497,"line":570},13,[495,572,573],{},"#    deploys.\n",[495,575,577],{"class":497,"line":576},14,[495,578,579],{},"AWS_PROFILE=studyflash-pulumi pnpm exec pulumi stack init prod \\\n",[495,581,583],{"class":497,"line":582},15,[495,584,585],{},"  --secrets-provider=\"gcpkms:\u002F\u002Fprojects\u002Fstudyflash-security\u002Flocations\u002Feurope-west6\u002FkeyRings\u002Fpulumi-state\u002FcryptoKeys\u002Fpulumi-state\"\n",[495,587,589],{"class":497,"line":588},16,[495,590,520],{"emptyLinePlaceholder":519},[495,592,594],{"class":497,"line":593},17,[495,595,596],{},"# 4. Set the package.json entry so Pulumi finds your TS:\n",[495,598,600],{"class":497,"line":599},18,[495,601,602],{},"#       \"main\": \"infra.ts\"\n",[495,604,606],{"class":497,"line":605},19,[495,607,608],{},"#    Pulumi reads package.json's `main` field, NOT Pulumi.yaml's\n",[495,610,612],{"class":497,"line":611},20,[495,613,614],{},"#    runtime.options.main. Don't burn an hour on this.\n",[495,616,618],{"class":497,"line":617},21,[495,619,520],{"emptyLinePlaceholder":519},[495,621,623],{"class":497,"line":622},22,[495,624,625],{},"# 5. Wrap apply in `infisical run` so secrets at \u002Finfra\u002F\u003Cstack>\u002F (or\n",[495,627,629],{"class":497,"line":628},23,[495,630,631],{},"#    \u002Finternal\u002F\u003Cservice>\u002F) show up as env vars. Add a `pnpm run up`\n",[495,633,635],{"class":497,"line":634},24,[495,636,637],{},"#    script in package.json that does the wrapping; do NOT invoke\n",[495,639,641],{"class":497,"line":640},25,[495,642,643],{},"#    `pulumi up` directly from CI or local shells.\n",[296,645,646,647,650,651,654,655,657,658,661],{},"Pre-reqs for any new stack contributor: ",[293,648,649],{},"AWS_PROFILE=studyflash-pulumi","\nconfigured in ",[293,652,653],{},"~\u002F.aws\u002Fcredentials"," (for the R2 state backend), and GCP\nApplication Default Credentials with access to the ",[293,656,317],{},"\nKMS key (",[293,659,660],{},"gcloud auth application-default login",").",[320,663,665],{"id":664},"lessons-read-before-adding-a-new-stack","Lessons (read before adding a new stack)",[667,668,670],"h3",{"id":669},"provider-sourcing-use-pulumis-tf-bridge-never-a-third-party-npm-package","Provider sourcing — use Pulumi's TF bridge, never a third-party NPM package",[296,672,673,674,677],{},"For any vendor where Pulumi doesn't ship an official ",[293,675,676],{},"@pulumi\u002F\u003Cvendor>","\npackage, generate the SDK locally from the vendor's own Terraform\nprovider:",[486,679,681],{"className":488,"code":680,"language":490,"meta":491,"style":491},"cd infra\u002F\u003Cstack>\npulumi package add terraform-provider \u003COrg>\u002F\u003Cprovider>\n",[293,682,683,688],{"__ignoreMap":491},[495,684,685],{"class":497,"line":498},[495,686,687],{},"cd infra\u002F\u003Cstack>\n",[495,689,690],{"class":497,"line":504},[495,691,692],{},"pulumi package add terraform-provider \u003COrg>\u002F\u003Cprovider>\n",[296,694,695,696,699,700,703,704,708,709,711,712,715,716,719],{},"This emits a typed SDK at ",[293,697,698],{},"sdks\u002F\u003Cprovider>\u002F",", importable as\n",[293,701,702],{},"@pulumi\u002F\u003Cprovider>",". Trust chain: ",[705,706,707],"strong",{},"vendor's TF provider → Pulumi\ncodegen → us",". No third-party intermediary. The ",[293,710,305],{},"'s\n",[293,713,714],{},"packages:"," block is the lock; anyone running ",[293,717,718],{},"pulumi install","\nregenerates the SDK from there.",[296,721,722,725,726,729,730,733,734,737],{},[705,723,724],{},"Do not"," depend on community NPM packages of the form ",[293,727,728],{},"pulumi-X","\n(e.g. ",[293,731,732],{},"pulumi-infisical"," from ",[293,735,736],{},"hckhanh","). They look convenient but they\nadd an unrelated single-maintainer dependency on top of the same TF\nprovider Pulumi can codegen for us natively. Past mistake; don't repeat.",[667,739,741],{"id":740},"mark-pulumi-emitted-infisical-secrets","Mark Pulumi-emitted Infisical secrets",[296,743,744,745,748,749,752,753,756,757,760],{},"Tag every ",[293,746,747],{},"infisical.Secret"," resource with the ",[293,750,751],{},"pulumi-managed"," Infisical\ntag (declare a ",[293,754,755],{},"infisical.SecretTag"," resource once per stack and reuse\nits id via ",[293,758,759],{},"tagIds","). Hand-edited values stay untagged; the UI then\ndistinguishes them at a glance.",[667,762,764],{"id":763},"keep-deploy-auth-out-of-pulumiyaml","Keep deploy auth out of Pulumi.yaml",[296,766,767,768,771,772,775,776,779,780,783],{},"Deploy-time creds (",[293,769,770],{},"PULUMI_BACKEND_URL",", R2 keys, provider tokens, …)\nlive in Infisical, never in committed config. The canonical ",[293,773,774],{},"pnpm run up"," shape wraps the apply in ",[293,777,778],{},"infisical run --path \u002Finfra\u002F\u003Cstack>\u002F",",\nso static secrets at that path show up as env vars to Pulumi.\nCross-folder references (",[293,781,782],{},"${prod.\u003Cother-path>.\u003CKEY>}",") resolve\nclient-side and let one folder reference another without duplicating\nvalues.",[667,785,787],{"id":786},"ssh","SSH",[296,789,790,791,794,795,798,799,802],{},"Use Infisical to broker SSH connections via Dynamic Secrets — short-lived\ncerts minted by ",[293,792,793],{},"packages\u002Fdevtools\u002Flease-ssh-cert.ts",". The Hetzner VMs\nprovisioned by ",[293,796,797],{},"learning-api"," and ",[293,800,801],{},"openreplay"," are the typical targets.",[804,805,806],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":491,"searchDepth":504,"depth":504,"links":808},[809,810,811],{"id":322,"depth":504,"text":323},{"id":483,"depth":504,"text":484},{"id":664,"depth":504,"text":665,"children":812},[813,814,815,816],{"id":669,"depth":510,"text":670},{"id":740,"depth":510,"text":741},{"id":763,"depth":510,"text":764},{"id":786,"depth":510,"text":787},"Pulumi projects for everything we self-host. One project per stack\n(infra\u002F\u003Cstack>\u002F), each with its own Pulumi.yaml, index.ts, and\nPulumi.\u003Cenv>.yaml. Stacks are independent — they share the R2 state\nbackend and the pulumi-state GCP KMS key for secret encryption,\nbut otherwise don't depend on each other.","md",null,{},{"title":171,"description":817},"infra\u002FREADME","SuiGyp_SoCNhcKV09kQBdMc4tFHNsb0H1sozqA6KZnc",1779007962946]