Markdown Muse is deployed to GCP in three parts:
- frontend: static web build on Firebase Hosting
- AI API: Cloud Run service
- TeX validation service: separate Cloud Run service for XeLaTeX validation and PDF compilation
The desktop profile is not part of the GCP deployment path. GCP should serve the
web profile only.
Recommended custom-domain topology for this repo:
https://app.YOUR_DOMAINserves the frontend from Firebase Hosting- the browser also calls
https://app.YOUR_DOMAIN/api/... - Firebase Hosting rewrites
/api/**to the Cloud Rundocsyservice - use a separate
api.YOUR_DOMAINonly if you intentionally want split frontend/API domains
Create a web deployment env file or inject the same values from CI/CD.
Required frontend values:
VITE_APP_PROFILE=webVITE_AI_API_BASE_URL=https://app.YOUR_DOMAIN
Example:
echo VITE_APP_PROFILE=web > .env.production.local
echo VITE_AI_API_BASE_URL=https://app.YOUR_DOMAIN >> .env.production.localnpm ci
npm run build:webThe generated dist/ folder is the deployable frontend artifact.
Deploy the static bundle with Firebase Hosting.
firebase deploy --only hostingFirebase Hosting must rewrite unknown SPA paths such as /editor to
/index.html.
This repository already includes that rewrite in firebase.json.
The same config also rewrites /api/** to the Cloud Run docsy service, so
single-domain custom-domain deploys do not need a separate api subdomain.
Firebase Hosting only forwards the __session cookie to rewritten backends, so deployed Google Workspace auth sessions must use __session rather than custom secure cookie names.
Use cache policy appropriate for hashed assets:
index.html: short TTL or no-cacheassets/*with hashed filenames: long TTL
This project already emits hashed asset filenames, so CDN caching is safe for
most files under dist/assets/.
Use Dockerfile.ai locally or cloudbuild.ai.yaml in Cloud Build.
gcloud builds submit \
--config cloudbuild.ai.yaml \
--substitutions=_IMAGE_URI=REGION-docker.pkg.dev/PROJECT/REPO/markdown-muse-aiGOOGLE_GENAI_USE_VERTEXAIGOOGLE_CLOUD_PROJECTGOOGLE_CLOUD_LOCATIONGEMINI_MODELGEMINI_FALLBACK_MODELAI_ALLOWED_ORIGINAI_MAX_REQUEST_BYTESAI_DIAGNOSTICS_TOKENAI_SERVER_PORTTEX_SERVICE_BASE_URLTEX_SERVICE_AUTH_TOKENTEX_ALLOW_ALL_PACKAGESTEX_ALLOW_RAW_DOCUMENTTEX_ALLOW_RESTRICTED_COMMANDSTEX_ALLOWED_PACKAGESWORKSPACE_FRONTEND_ORIGINGOOGLE_OAUTH_REDIRECT_URIGOOGLE_OAUTH_PUBLISHING_STATUSGOOGLE_WORKSPACE_SCOPE_PROFILE- Cloud Firestore enabled in the target project for shared Google Workspace state
Recommended values:
GEMINI_MODEL=gemini-2.5-flashGEMINI_FALLBACK_MODEL=gemini-2.5-flash-liteAI_SERVER_PORT=8080AI_ALLOWED_ORIGIN=https://app.YOUR_DOMAINAI_MAX_REQUEST_BYTES=2097152AI_DIAGNOSTICS_TOKENshould be provided through a Secret Manager-backed env when internal diagnostics access is neededWORKSPACE_FRONTEND_ORIGIN=https://app.YOUR_DOMAINGOOGLE_OAUTH_REDIRECT_URI=https://app.YOUR_DOMAIN/api/auth/google/callbackGOOGLE_OAUTH_PUBLISHING_STATUS=testingGOOGLE_WORKSPACE_SCOPE_PROFILE=restricted- Cloud Firestore enabled with a database created in the same project
TEX_ALLOW_ALL_PACKAGES=trueTEX_ALLOW_RAW_DOCUMENT=trueTEX_ALLOW_RESTRICTED_COMMANDS=falseTEX_ALLOWED_PACKAGES=amsmath,amssymb,amsthm,array,booktabs,caption,enumitem,etoolbox,fancyhdr,float,fontspec,geometry,graphicx,hyperref,inputenc,latexsym,listings,longtable,makecell,mathtools,multirow,setspace,soul,tabularx,tcolorbox,titlesec,ulem,xcolor,xeCJKTEX_SERVICE_BASE_URL=https://YOUR_TEX_CLOUD_RUN_URL
AI_ALLOWED_ORIGIN should be the exact frontend origin, not *. The deploy
validator now treats wildcard CORS as invalid outside local development.
When Firebase Hosting fronts Cloud Run through the existing /api/** rewrite,
VITE_AI_API_BASE_URL should usually be the frontend custom domain so browser
traffic stays same-origin.
For external production Google OAuth, keep managed preview domains such as
*.web.app and *.run.app out of the final config.
LaTeX mode sends raw LaTeX to the backend. Full preambles and
\documentclass ... \begin{document} ... \end{document} wrappers require
TEX_ALLOW_RAW_DOCUMENT=true.
Public/demo deployments now default to TEX_ALLOW_ALL_PACKAGES=true, which
disables package allowlist enforcement while still keeping dangerous
file/process primitives blocked with TEX_ALLOW_RESTRICTED_COMMANDS=false.
gcloud run deploy markdown-muse-ai \
--image REGION-docker.pkg.dev/PROJECT/REPO/markdown-muse-ai \
--region REGION \
--allow-unauthenticated \
--set-env-vars GEMINI_MODEL=gemini-2.5-flash,GEMINI_FALLBACK_MODEL=gemini-2.5-flash-lite,AI_SERVER_PORT=8080,AI_ALLOWED_ORIGIN=https://app.YOUR_DOMAIN,WORKSPACE_FRONTEND_ORIGIN=https://app.YOUR_DOMAIN,GOOGLE_OAUTH_REDIRECT_URI=https://app.YOUR_DOMAIN/api/auth/google/callback,GOOGLE_OAUTH_PUBLISHING_STATUS=testing,GOOGLE_WORKSPACE_SCOPE_PROFILE=restricted,TEX_SERVICE_BASE_URL=https://YOUR_TEX_CLOUD_RUN_URLCloud Run uses Vertex AI auth through the runtime service account. Do not wire a Gemini API key into this service. Grant the runtime service account Vertex AI permissions instead.
Run the public-deploy validator before switching the OAuth app to production:
npm run check:public-deployFor the hosted https://docsy.cyou topology, pin the expected public origin during validation so managed Google hosts fail fast even while the OAuth app is still in testing mode:
PUBLIC_DEPLOY_EXPECTED_FRONTEND_ORIGIN=https://docsy.cyou npm run check:public-deployGoogle Workspace OAuth state must be shared across Cloud Run instances. This repo now defaults to the Firestore repository backend on Cloud Run. Before rollout, enable Firestore and create a database in the deploy project.
You do not need to manually git clone in GCP.
github.com repository is your source of truth, and CI handles the build/deploy.
Use:
Required repository secrets:
GCP_SA_KEY(service account JSON, or replace with workload identity auth)GCP_PROJECT_IDGCP_FIREBASE_PROJECT_ID(optional override when Hosting lives in a separate production project)GCP_AI_ALLOWED_ORIGIN(exact frontend origin)GCP_WORKSPACE_FRONTEND_ORIGIN(exact frontend origin used for OAuth redirects)GCP_GOOGLE_OAUTH_REDIRECT_URI(exact callback URL on the frontend custom domain)GCP_GOOGLE_OAUTH_PUBLISHING_STATUS(testingorproduction)GCP_GOOGLE_WORKSPACE_SCOPE_PROFILE(restrictedorreduced)GCP_AI_DIAGNOSTICS_TOKEN_SECRET_NAME(optional, defaults toai-diagnostics-token)GCP_TEX_SERVICE_AUTH_TOKEN_SECRET_NAME(optional, defaults totex-service-auth-token)GCP_TEX_SERVICE_BASE_URLGCP_WEB_VITE_AI_API_BASE_URL(optional override; defaults toGCP_WORKSPACE_FRONTEND_ORIGINin the repo workflows)FIREBASE_SERVICE_ACCOUNT_URBAN_DDS
The split workflows do:
deploy-tex.yml- deploy only the TeX service
- health check
GET /health
deploy-ai.yml- deploy only the AI API
- health checks
GET /api/ai/healthandGET /api/tex/health - reads
GCP_TEX_SERVICE_BASE_URLfrom secrets
deploy-web.yml- build and deploy only the frontend
- uses
GCP_WEB_VITE_AI_API_BASE_URLwhen set, otherwise defaults to the configured frontend origin
deploy-full-stack.yml- manual coordinated release workflow
- runs TeX -> AI -> web in sequence
Trigger behavior:
- each split workflow includes its own workflow file in
push.paths - deployment validation script changes trigger the relevant workflows
deploy-full-stack.ymlremains manual by design and does not auto-run on push
Recommended usage:
- day-to-day deploys: use the split workflows
- coordinated releases or contract changes: use
deploy-full-stack.yml
Diagnostics secret fallback:
- if
GCP_AI_DIAGNOSTICS_TOKEN_SECRET_NAMEis unset, deploy workflows fall back toai-diagnostics-token - internal diagnostics verification now checks Secret Manager directly and logs a clear skip when the secret does not exist
Use Dockerfile.tex locally or cloudbuild.tex.yaml in Cloud Build.
gcloud builds submit \
--config cloudbuild.tex.yaml \
--substitutions=_IMAGE_URI=REGION-docker.pkg.dev/PROJECT/REPO/markdown-muse-texTEX_SERVICE_PORTTEX_COMPILE_TIMEOUT_MSTEX_MAX_SOURCE_BYTESTEX_MAX_REQUEST_BYTESTEX_MAX_CONCURRENCYTEX_SERVICE_AUTH_TOKENTEX_ALLOW_ALL_PACKAGESTEX_ALLOW_RAW_DOCUMENTTEX_ALLOW_RESTRICTED_COMMANDSTEX_ALLOWED_PACKAGES
Recommended values:
TEX_SERVICE_PORT=8081TEX_COMPILE_TIMEOUT_MS=15000TEX_MAX_SOURCE_BYTES=300000TEX_MAX_REQUEST_BYTES=400000TEX_MAX_CONCURRENCY=2TEX_ALLOW_ALL_PACKAGES=trueTEX_ALLOW_RAW_DOCUMENT=trueTEX_ALLOW_RESTRICTED_COMMANDS=falseTEX_ALLOWED_PACKAGES=amsmath,amssymb,amsthm,array,booktabs,caption,enumitem,etoolbox,fancyhdr,float,fontspec,geometry,graphicx,hyperref,inputenc,latexsym,listings,longtable,makecell,mathtools,multirow,setspace,soul,tabularx,tcolorbox,titlesec,ulem,xcolor,xeCJK
The TeX service is called by the AI API, not directly by the browser. The
frontend should continue to use only VITE_AI_API_BASE_URL.
This repo's public/demo deployment keeps full raw LaTeX documents enabled while
allows arbitrary installed LaTeX packages while still blocking dangerous
file/process primitives with
TEX_ALLOW_RESTRICTED_COMMANDS=false.
TEX_ALLOWED_PACKAGES remains available only for stricter/private deployments
that opt back into allowlist enforcement with TEX_ALLOW_ALL_PACKAGES=false.
The current TeX image is a coverage-first profile, not a minimal fast-build profile. It intentionally includes a broader curated TeX Live package set for common resumes, papers, reports, citations, and layout packages. Deploy time and image size are therefore higher than a minimal XeLaTeX image.
When TeX package coverage changes, routine redeploy normally requires only:
deploy-tex.yml
If coverage is still insufficient after the curated package set, the next
escalation path is evaluating texlive-full.
- Create or reuse an Artifact Registry repository for the TeX image.
- Create a Secret Manager secret for
tex-service-auth-token. - Deploy a new Cloud Run service such as
docsy-texusingDockerfile.texwith--no-allow-unauthenticated. - Grant
roles/run.invokeron the TeX service to the AI service account. - Note the resulting service URL and pass it to the AI service as
TEX_SERVICE_BASE_URL. - Use the same
TEX_SERVICE_AUTH_TOKENsecret in both the TeX service and the AI service.
The AI service calls the TeX service with:
- a Cloud Run identity token in
Authorization: Bearer ... - an app-level shared secret in
X-Docsy-Tex-Token - request-time LaTeX validation that rejects raw full-document compilation unless
TEX_ALLOW_RAW_DOCUMENT=true - package allowlist enforcement is skipped when
TEX_ALLOW_ALL_PACKAGES=true - file/process primitives remain blocked unless
TEX_ALLOW_RESTRICTED_COMMANDS=true \usepackage{...}declarations are checked againstTEX_ALLOWED_PACKAGESunless restricted commands are fully enabled
If any workspace state file or OAuth token is committed to Git history, follow:
- Health endpoint:
GET https://YOUR_TEX_CLOUD_RUN_URL/health - direct health checks require the same Cloud Run identity token and
X-Docsy-Tex-Tokenheader as runtime traffic - Roll back by redeploying the previous TeX image revision and then the AI service revision that points to it.
- Deploy the TeX service first.
- Confirm
GET /healthworks from the TeX Cloud Run URL. - Deploy the AI API with
TEX_SERVICE_BASE_URLpointing to that TeX URL. - Confirm
GET /api/ai/healthandGET /api/tex/healthwork from the AI API URL. - Build the frontend with
VITE_AI_API_BASE_URLpointing tohttps://app.YOUR_DOMAINso Firebase Hosting keeps browser API traffic same-origin. Use the API origin only for split-domain deployments. - Deploy the frontend to Firebase Hosting.
- Confirm SPA rewrites for routes such as
/editor. - Turn on appropriate caching for hashed assets.
도메인이 없을 때는 기본 URL만 써도 바로 배포할 수 있습니다.
- AI API: Cloud Run 기본 URL 사용
- 예)
https://markdown-muse-ai-xxxxx.a.run.app
- 예)
- 프런트엔드: Cloud Storage 정적 호스팅 기본 URL 사용
- 예)
https://storage.googleapis.com/YOUR_BUCKET/index.html
- 예)
- GitHub Secrets
GCP_PROJECT_IDGCP_SA_KEYFIREBASE_SERVICE_ACCOUNT_URBAN_DDSGCP_AI_ALLOWED_ORIGIN는 임시 배포 단계에서는 생략 가능(워크플로우에서 기본*로 동작)
- AI CORS
- 도메인 미보유면 우선
AI_ALLOWED_ORIGIN=*이 편합니다. - 보안상 위험이 있다면 도메인이 생기면 추후
https://storage.googleapis.com/YOUR_BUCKET형태로 정확히 제한하세요.
- Cloud Console에서 Artifact Registry, Secret Manager, Cloud Run, Cloud Storage 항목이 준비되었는지 확인
- GitHub Secrets를 등록
- GitHub에
main푸시 또는 Actions 수동 실행 - 배포 완료 후 확인:
https://YOUR_CLOUD_RUN_URL/api/ai/health가 200https://storage.googleapis.com/YOUR_BUCKET/index.html접속/editor를 직접 입력하고 새로고침해도 동작
docs에 있는cloudbuild.ai.yaml은 Cloud Run 배포용이고, 프런트는 기존처럼build:web결과를 Cloud Storage에 올려 사용하는 방식입니다.
Verify all of the following:
GET https://YOUR_TEX_CLOUD_RUN_URL/healthreturns successGET https://app.YOUR_DOMAIN/api/ai/healthreturns successGET https://app.YOUR_DOMAIN/api/tex/healthreturns success- browser requests from the frontend origin pass CORS
- direct navigation to
/editorworks - refresh on
/editorworks - share-link hash routes still open correctly
- web build loads the
webprofile, not the desktop profile googleOAuthPublishingStatusandgoogleWorkspaceScopeProfilein/api/ai/healthmatch the intended rollout mode
Required:
GOOGLE_GENAI_USE_VERTEXAIGOOGLE_CLOUD_PROJECTGOOGLE_CLOUD_LOCATIONGEMINI_MODELGEMINI_FALLBACK_MODELAI_ALLOWED_ORIGINTEX_SERVICE_BASE_URLTEX_SERVICE_AUTH_TOKENGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETGOOGLE_OAUTH_REDIRECT_URIGOOGLE_OAUTH_PUBLISHING_STATUSGOOGLE_WORKSPACE_SCOPE_PROFILEWORKSPACE_FRONTEND_ORIGIN
Recommended:
GOOGLE_WORKSPACE_SCOPESWORKSPACE_STATE_PATH
Expected shape:
GOOGLE_GENAI_USE_VERTEXAI=trueGOOGLE_CLOUD_PROJECT=YOUR_GCP_PROJECTGOOGLE_CLOUD_LOCATION=asia-northeast3AI_ALLOWED_ORIGIN=https://app.YOUR_DOMAINGEMINI_MODEL=gemini-2.5-flashGEMINI_FALLBACK_MODEL=gemini-2.5-flash-liteTEX_SERVICE_BASE_URL=https://YOUR_TEX_CLOUD_RUN_URLWORKSPACE_FRONTEND_ORIGIN=https://app.YOUR_DOMAINGOOGLE_OAUTH_REDIRECT_URI=https://app.YOUR_DOMAIN/api/auth/google/callbackGOOGLE_OAUTH_PUBLISHING_STATUS=testing|productionGOOGLE_WORKSPACE_SCOPE_PROFILE=restricted|reduced- the AI service account has
roles/run.invokeron the TeX service
Required:
GCP_SA_KEYGCP_PROJECT_IDGCP_FIREBASE_PROJECT_IDGCP_AI_ALLOWED_ORIGINGCP_WORKSPACE_FRONTEND_ORIGINGCP_GOOGLE_OAUTH_REDIRECT_URIGCP_GOOGLE_OAUTH_PUBLISHING_STATUSGCP_GOOGLE_WORKSPACE_SCOPE_PROFILEGCP_TEX_SERVICE_AUTH_TOKEN_SECRET_NAME(optional, defaults totex-service-auth-token)GCP_TEX_SERVICE_BASE_URLFIREBASE_SERVICE_ACCOUNT_URBAN_DDS
Recommended:
GCP_WEB_VITE_AI_API_BASE_URL(set it explicitly tohttps://app.YOUR_DOMAINor let the workflow default it fromGCP_WORKSPACE_FRONTEND_ORIGIN)
Expected shape:
GCP_AI_ALLOWED_ORIGIN=https://app.YOUR_DOMAINGCP_WORKSPACE_FRONTEND_ORIGIN=https://app.YOUR_DOMAINGCP_GOOGLE_OAUTH_REDIRECT_URI=https://app.YOUR_DOMAIN/api/auth/google/callbackGCP_GOOGLE_OAUTH_PUBLISHING_STATUS=testing|productionGCP_GOOGLE_WORKSPACE_SCOPE_PROFILE=restricted|reducedGCP_TEX_SERVICE_BASE_URL=https://YOUR_TEX_CLOUD_RUN_URLGCP_WEB_VITE_AI_API_BASE_URL=https://app.YOUR_DOMAIN
Required:
VITE_APP_PROFILE=webVITE_AI_API_BASE_URL=https://app.YOUR_DOMAIN
Symptom:
Google Workspace API is not reachableUnexpected token '<'- frontend requests appear to target
/api
Cause:
- the frontend was built without
VITE_AI_API_BASE_URL - the frontend custom domain is not forwarding
/api/**to Cloud Run - the browser receives
index.htmlinstead of JSON from the AI API
Fix:
- Confirm the Cloud Run service is healthy:
GET https://app.YOUR_DOMAIN/api/ai/health
- Set frontend build env:
VITE_APP_PROFILE=webVITE_AI_API_BASE_URL=https://app.YOUR_DOMAIN
- Rebuild and redeploy the frontend
- Keep the Firebase Hosting
/api/**rewrite to Cloud Run active infirebase.json
Symptom:
- Google login fails after consent
- callback errors mention redirect mismatch or missing callback parameters
Fix:
- Cloud Run env:
GOOGLE_OAUTH_REDIRECT_URI=https://app.YOUR_DOMAIN/api/auth/google/callbackWORKSPACE_FRONTEND_ORIGIN=https://app.YOUR_DOMAIN
- Google Cloud OAuth client:
- add the exact same callback URL to
Authorized redirect URIs
- add the exact same callback URL to
Symptom:
- browser requests to Cloud Run fail only in deployed frontend
Fix:
- set
AI_ALLOWED_ORIGIN=https://app.YOUR_DOMAIN - use the exact frontend origin, including protocol
Symptom:
- preview or PDF export fails with:
Raw LaTeX document compilation is disabled for this deployment. Submit document body content instead of a full preamble/document wrapper.
Cause:
- the deployed AI service and/or TeX service revision drifted to
TEX_ALLOW_RAW_DOCUMENT=false - this usually happens after a redeploy when workflow defaults or Cloud Build substitutions no longer match the intended demo policy
Fix:
- confirm both Cloud Run services have
TEX_ALLOW_RAW_DOCUMENT=true - keep
TEX_ALLOW_RESTRICTED_COMMANDS=falseunless you explicitly trust file/process primitives - redeploy through the repo-managed workflows so AI and TeX receive the same TeX policy values
Symptom:
- preview or PDF export fails with:
LaTeX source requests package "..." , which is not on the allowed package list for this deployment.
Cause:
- the deployment is still running with
TEX_ALLOW_ALL_PACKAGES=false - or a stricter/private environment intentionally opted back into allowlist mode
Fix:
- for public/demo deployments, set
TEX_ALLOW_ALL_PACKAGES=trueon both AI and TeX services and redeploy - for stricter/private deployments, extend
TEX_ALLOWED_PACKAGESonly if you intentionally want allowlist mode - if the package comes from custom/raw user LaTeX outside the built-in feature set, leave it blocked unless you explicitly trust and support it
Symptom:
- the package-policy error is gone, but compile logs still report that a package or style file cannot be found
Cause:
- the TeX package is allowed by policy but not installed in the container image
- the current image is broad (
texlive-latex-extra,texlive-publishers,texlive-science, etc.) but it is still not equivalent totexlive-full
Fix:
- treat this as TeX image coverage, not policy enforcement
- inspect compile logs to identify the missing package
- either install the missing TeX Live collection(s) or evaluate moving to
texlive-fullif package churn remains high
Symptom:
- workflow logs show
Unable to access required Secret Manager secret ... - the secret is visible from an operator shell in the target project
Cause:
- the GitHub Actions service account behind
GCP_SA_KEYdoes not have access to the secret metadata or latest version - workflow preflight now checks actual secret access, not just the configured name
Fix:
- check the
Show active GCP deploy principalstep in the same workflow run and use that service account email for IAM binding - grant the GitHub Actions service account
roles/secretmanager.secretAccessoron the required secret - for diagnostics checks, do the same for
ai-diagnostics-tokenif you want the internal health verification to run
Symptom:
GET /api/ai/healthreturns 200 JSON- browser
POST /api/ai/agent/turnrequests still fail with 5xx
Check these first:
- Firestore is enabled and has a database in the same GCP project as the AI Cloud Run service
- the Cloud Run runtime service account can access Firestore in that project
AI_ALLOWED_ORIGIN,WORKSPACE_FRONTEND_ORIGIN, andGOOGLE_OAUTH_REDIRECT_URIall match the deployed frontend origin when using Firebase Hosting/api/**rewrites
Recommended post-deploy smoke test:
node scripts/check-ai-runtime-smoke.mjs --origin https://app.YOUR_DOMAINThat smoke test verifies:
GET /api/ai/healthreturns a configuration-level JSON responseGET /api/auth/sessionwithout cookiesPOST /api/ai/agent/turnwith a minimal payload returns noagentStatus
/api/ai/health confirms that the required Vertex AI env vars are present. It
does not prove the configured model can actually serve requests. The smoke test
is the readiness check that exercises a real Agent turn.
npm run buildis for desktop-oriented output and should not be used for the static GCP frontend deployment.npm run build:webis the correct production build for GCP frontend hosting.- The web profile intentionally defers AI, knowledge, history, structured editing, document tools, and advanced blocks until needed.