Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/llm-docs/publishing-architecture.md
6832 views
---
main_commit: 76b946025 analyzed_date: 2026-02-16 key_files: - src/publish/provider-types.ts - src/publish/provider.ts - src/publish/publish.ts - src/publish/config.ts - src/publish/common/publish.ts - src/publish/common/bundle.ts - src/publish/common/account.ts - src/publish/types.ts - src/command/publish/cmd.ts - src/publish/posit-connect-cloud/posit-connect-cloud.ts - src/publish/posit-connect-cloud/api/index.ts - src/publish/posit-connect-cloud/api/types.ts
---

Quarto Publishing Architecture

How quarto publish works internally. Covers the provider interface, publish patterns, account management, and the end-to-end publish flow.

Provider Interface

File: src/publish/provider-types.ts

Every publish target implements PublishProvider:

export interface PublishProvider { name: string; // e.g. "netlify", "rsconnect", "quarto-pub" description: string; // Human-readable name requiresServer: boolean; // true if user provides server URL (e.g. rsconnect) hidden?: boolean; // Hide from provider selection listOriginOnly?: boolean; // Only list in origin project accountTokens(): Promise<AccountToken[]>; removeToken(token: AccountToken): void; authorizeToken(options: PublishOptions, target?: PublishRecord): Promise<AccountToken | undefined>; resolveTarget(account: AccountToken, target: PublishRecord): Promise<PublishRecord | undefined>; publish( account: AccountToken, type: "document" | "site", input: string, title: string, slug: string, render: (flags?: RenderFlags) => Promise<PublishFiles>, options: PublishOptions, target?: PublishRecord ): Promise<[PublishRecord | undefined, URL | undefined]>; isUnauthorized(error: Error): boolean; isNotFound(error: Error): boolean; }

Key types:

  • AccountToken — stored credential with name, server?, token (generic string or structured object)

  • AccountTokenType"authorized" (user-stored) or "environment" (env var)

  • PublishRecord — entry in _publish.yml with id, url, code?

  • PublishFiles{ baseDir: string, rootFile: string, files: string[], metafiles?: string[] }

Provider Registration

File: src/publish/provider.ts

Providers are imported and added to kPublishProviders:

const kPublishProviders = [ quartoPubProvider, ghpagesProvider, rsconnectProvider, netlifyProvider, positConnectCloudProvider, confluenceProvider, huggingfaceProvider, ];

Discovery functions: publishProviders(), findProvider(name).

There is also a deprecation warning for the old posit-cloud provider (removed in 142a8791f) that now suggests using posit-connect-cloud as an alternative.

Two Publish Patterns

Pattern A: File-by-file upload

Used by: quarto-pub, netlify

File: src/publish/common/publish.ts

Uses handlePublish() which:

  1. SHA-1 hashes each file in the rendered output

  2. Creates a deploy with a file manifest (listing all files + checksums)

  3. Server responds with which files it needs (doesn't already have)

  4. Uploads only changed files individually

  5. Activates the deploy

Providers using this pattern implement PublishHandler:

export interface PublishHandler { name: string; createSite(type, title, slug, account): Promise<[string, string]>; createDeploy(siteId, account, files): Promise<PublishDeploy>; getDeploy(deployId, account): Promise<PublishDeploy>; uploadDeployFile(deployId, path, fileBody, account): Promise<void>; }

Pattern B: Bundle upload

Used by: rsconnect (Posit Connect), posit-connect-cloud (Posit Connect Cloud)

File: src/publish/common/bundle.ts

Uses createBundle() which:

  1. Creates a manifest.json with file checksums, appmode, content_category, etc.

  2. Packages all files + manifest into a .tar.gz archive

  3. Returns { bundlePath: string, manifest: object }

The provider then uploads the entire bundle as a single blob and triggers deployment. Each provider using this pattern has its own publish logic (not via handlePublish()).

createBundle() signature:

function createBundle( type: "document" | "site", files: PublishFiles, tempContext: TempContext ): Promise<{ bundlePath: string; manifest: Record<string, unknown> }>

End-to-End Publish Flow

1. CLI Entry

File: src/command/publish/cmd.ts

quarto publish [provider] [path] invokes publishAction().

2. Resolve Deployment Target

File: src/publish/config.ts

Reads _publish.yml to find existing publish targets. Format:

- source: document.qmd provider-name: - id: site-or-content-id url: https://published-url.example.com

3. Select Provider

If not specified on CLI, user is prompted to choose from available providers.

4. Resolve Account

File: src/publish/common/account.ts

Account resolution order:

  1. Environment variable tokens (via provider's accountTokens())

  2. Stored tokens in ~/.quarto/publish/accounts/{provider}/accounts.json

  3. Interactive authorization (via provider's authorizeToken())

Token storage functions:

  • readAccessTokens<T>(provider) — reads stored tokens

  • writeAccessToken<T>(provider, token, compareFn) — writes/updates token

  • readAccessTokenFile(provider) — raw file path

5. Publish

File: src/publish/publish.ts

publishSite() or publishDocument() coordinates:

  1. Calls provider's publish(), passing a render callback

  2. Provider calls renderForPublish() (or render() directly for sites)

  3. Provider uploads and deploys

  4. Returns [PublishRecord, URL]

Important: Providers publishing documents should call renderForPublish() instead of render() directly. renderForPublish() wraps render() and stages the output: for HTML documents it copies document.htmlindex.html, for PDFs it creates a pdf.js viewer wrapper as index.html. Without this staging, the primary file name won't match index.html and bundle-based providers will fail.

6. Update _publish.yml

File: src/publish/config.ts

The returned PublishRecord is written back to _publish.yml for future republishing.

Account / Token Management

File: src/publish/common/account.ts

Storage

Tokens stored at: ~/.quarto/publish/accounts/{provider}/accounts.json

Format: JSON array of AccountToken objects. The token field is provider-specific (can be a string or structured object).

Authorization Patterns

Ticket-based auth (quarto-pub): authorizeAccessToken() in src/publish/common/account.ts

  • Opens browser to auth URL

  • Polls a ticket endpoint until user completes auth

  • Exchanges ticket for access token

API key auth (rsconnect): User provides API key directly or via env var.

OAuth Device Code (posit-connect-cloud): Uses RFC 8628 Device Code flow. See src/publish/posit-connect-cloud/api/index.ts for implementation.

Environment Variables

Each provider can check for env vars in accountTokens(). Convention:

  • QUARTO_PUB_AUTH_TOKEN

  • CONNECT_SERVER + CONNECT_API_KEY (rsconnect)

  • NETLIFY_AUTH_TOKEN

  • POSIT_CONNECT_CLOUD_ACCESS_TOKEN + POSIT_CONNECT_CLOUD_REFRESH_TOKEN + POSIT_CONNECT_CLOUD_ACCOUNT_ID (posit-connect-cloud)

Existing Providers Summary

ProviderPatternAuthrequiresServer
quarto-pubA (file-by-file)Ticket-based OAuthfalse
netlifyA (file-by-file)API key / env varfalse
rsconnectB (bundle)API key (Key <key>)true
ghpagesCustom (git push)Git credentialsfalse
confluenceCustomAPI tokentrue
posit-connect-cloudB (bundle)OAuth Device Code (RFC 8628)false
huggingfaceCustomHF tokenfalse

Reusable Utilities

UtilityFilePurpose
createBundle()src/publish/common/bundle.tstar.gz bundle with manifest.json
readAccessTokens<T>()src/publish/common/account.tsRead stored tokens
writeAccessToken<T>()src/publish/common/account.tsWrite/update token
authorizeAccessToken()src/publish/common/account.tsTicket-based auth flow
renderForPublish()src/publish/common/publish.tsRender + stage documents (HTML→index.html, PDF→pdf.js wrapper)
handlePublish()src/publish/common/publish.tsFile-by-file upload orchestration
withSpinner()src/core/console.tsProgress spinner display
completeMessage()src/core/console.tsSuccess/failure messages
createTempContext()src/core/temp.tsTemp file management
openUrl()src/core/shell.tsOpen URL in browser
isServerSession()src/core/platform.tsDetect headless/CI environment
ApiErrorsrc/publish/types.tsHTTP error type with status code