-
Notifications
You must be signed in to change notification settings - Fork 0
Deployment
The siamang.deploy subpackage turns a Questionnaire into a publicly reachable
URL. It is pluggable: backends (where responses are stored) and frontends
(where the static survey is hosted) are resolved by name through Python entry
points, so third-party packages can ship their own. The recommended entry point is
Questionnaire.deploy(...), which compiles, provisions, builds, and publishes in
one call.
from siamang.deploy import (
DeployPipeline, DeployResult, BackendConfig,
BackendAdapter, FrontendAdapter,
backend_factory, frontend_factory,
list_backends, list_frontends,
)result = survey.deploy(
backend="supabase", # name → BackendAdapter via entry points
frontend="vercel", # name → FrontendAdapter via entry points
backend_kwargs={...}, # forwarded to the backend adapter's __init__
frontend_kwargs={...}, # forwarded to the frontend adapter's __init__
**options, # quota=..., language=... → compile_questionnaire;
# ui=... / runtime=... → FrontendBuilder
) # -> DeployResultbackend defaults to "local" and frontend to "local", so a bare
survey.deploy() writes a local SQLite store and serves it from a background
FastAPI server. deploy() extracts ui=UIConfig(...) (see
Frontend and Theming) and runtime= (defaulting to
ReactRuntime() when not passed) itself and hands
them to the FrontendBuilder; the remaining **options are forwarded to
compilation — common ones are quota=[...] (see Quotas) and language=.
import siamang as sg
result = survey.deploy(
backend="supabase", frontend="vercel",
backend_kwargs={"url": "https://abc.supabase.co", "anon_key": "...", "service_key": "..."},
frontend_kwargs={"token": "...", "project_name": "political-trust-2026"},
)
print(result.url, result.dashboard)
df = result.collect() # pull accumulated responses laterA BackendAdapter provisions storage, reads responses, and answers quota checks.
class BackendAdapter:
name: str
def provision(self, schema: SurveySchema) -> BackendConfig: ...
def get_responses(self, survey_id: str) -> pd.DataFrame: ...
def check_quota(self, survey_id: str, variable: str, value: Any) -> bool: ...| Backend | name |
Storage | Key kwargs |
|---|---|---|---|
LocalBackend |
local |
SQLite file | path="survey.db" |
SupabaseBackend |
supabase |
Postgres + RLS |
url, anon_key, service_key, auto_provision=True
|
GoogleSheetsBackend |
gsheets |
Google Spreadsheet |
credentials_file, spreadsheet_id, apps_script_url
|
| Capability | local | supabase | gsheets |
|---|---|---|---|
| Zero external setup | ✅ | ❌ | ❌ |
| Public web submissions | ❌ (preview only) | ✅ | |
| Atomic quota counters | ✅ | ✅ | |
| High concurrency | ❌ | ✅ | ❌ (~100 req/100s) |
| Response dashboard | ❌ | ✅ | ✅ (the spreadsheet) |
-
LocalBackendcreates three tables —survey_meta,responses,quota_counters— and addsstore_response(...)andincrement_quota(...). It backssiamang preview. -
SupabaseBackenduses a single sharedresponsestable keyed bysurvey_id. Credentials fall back toSIAMANG_SUPABASE_URL/SIAMANG_SUPABASE_ANON_KEY/SIAMANG_SUPABASE_SERVICE_KEY(legacySURVLIB_*also accepted); the constructor raisesValueErrorif any are still empty. Withauto_provision=Trueit creates tables via anexec_sqlRPC you set up once; otherwise generate SQL withsiamang.deploy.backends.supabase.generate_migration_sql(). -
GoogleSheetsBackendwrites one row per response. It needs the optionalgsheetsextra (pip install "siamang[gsheets]"). For public deployments you must route submissions through an Apps Script proxy (apps_script_url) — see the security note indocs/reference/deploy.md.
@dataclass(frozen=True, slots=True)
class BackendConfig:
backend: str
survey_id: str
settings: dict[str, Any] = {} # frontend-safe (URLs, anon keys)
internal: dict[str, Any] = {} # server-only secrets
dashboard_url: str | None = NoneReturned by provision(). It draws the boundary between server-only secrets
(internal) and frontend-safe values (settings). Only settings and
dashboard_url ever cross into the deployed bundle.
A FrontendAdapter receives the compiled bundle and the BackendConfig, hosts the
static files, and returns the public URL.
class FrontendAdapter:
name: str
def publish(self, bundle: SurveyBundle, config: BackendConfig) -> str: ...| Frontend | name |
Host | Key kwargs |
|---|---|---|---|
LocalFrontend |
local |
Background FastAPI server |
host, port=0, open_browser
|
VercelFrontend |
vercel |
Vercel |
token, team_id, project_name
|
NetlifyFrontend |
netlify |
Netlify CDN |
token, site_id, site_name
|
-
LocalFrontendserves the bundle and forwardsPOST /responsesandPOST /quota-checkto the backend. Stop it withlocal_frontend.stop();siamang previewblocks until Ctrl+C. -
VercelFrontenddeploys via the Vercel REST API whentokenis set (falls back toVERCEL_TOKEN); with a token but no REST path it falls back tonpx vercel --prod --token <token>; without a token it writes.vercel_deploy_<survey_id>/for manual upload. It injects a strictvercel.json(CSP,X-Frame-Options: DENY, asset caching). WithUIConfig.enable_analytics=Truethe Vercel Web Analytics script is injected into the bundle's page itself. -
NetlifyFrontendZIP-uploads via the REST API whentokenis set (falls back toNETLIFY_AUTH_TOKEN, thennpx netlify deploy --prod, then a local write). It injects security headers via_headersand SPA routing via_redirects. Extra methods:get_deploy_status(deploy_id),list_deploys().
| Use case | Backend | Frontend |
|---|---|---|
| Local development / testing | local |
local |
| Small survey, shared with a team | gsheets |
netlify |
| Production, high concurrency | supabase |
vercel or netlify
|
| Offline / air-gapped (HTML bundle) | local |
local |
@dataclass(frozen=True, slots=True)
class DeployResult:
url: str
backend: str
frontend: str
survey_id: str = ""
dashboard: str | None = None
deployed_at: datetime = ...
backend_ref: BackendAdapter | None = None
frontend_ref: FrontendAdapter | None = None
extras: dict[str, Any] = {}
def collect(self) -> pd.DataFrame: ...What survey.deploy(...) returns. collect() reuses the cached backend_ref to
fetch accumulated responses as a DataFrame; it raises RuntimeError if the
reference is missing (which only happens for a hand-built DeployResult).
responses = result.collect()
data = sg.SurveyData(frame=responses, variables=survey.variables, questionnaire=survey)
print(data.report.freq("trust").to_markdown())from siamang.deploy import list_backends, list_frontends, backend_factory, frontend_factory
list_backends() # ['gsheets', 'local', 'supabase']
list_frontends() # ['local', 'netlify', 'vercel']
backend_factory("supabase") # <class 'SupabaseBackend'>
frontend_factory("netlify") # <class 'NetlifyFrontend'>backend_factory/frontend_factory look up names in the siamang.backends /
siamang.frontends entry-point groups first (so plugins win), then fall back to the
built-in registry. From tests/test_adapters.py, backends/frontends initialise
straight from environment variables:
import os
os.environ["SIAMANG_GSHEETS_CREDENTIALS_FILE"] = "/path/creds.json"
os.environ["SIAMANG_GSHEETS_SPREADSHEET_ID"] = "sheet_123"
backend = backend_factory("gsheets")() # picks up both env vars
assert backend.credentials_file == "/path/creds.json"
os.environ["NETLIFY_AUTH_TOKEN"] = "nfp_..."
frontend = frontend_factory("netlify")()
assert frontend.token == "nfp_..."@dataclass(slots=True)
class DeployPipeline:
backend: BackendAdapter
frontend: FrontendAdapter
builder: FrontendBuilder
def run(self, survey: Questionnaire, *, options: dict | None = None) -> DeployResult: ...Questionnaire.deploy(...) builds this for you, but you can wire it directly. run():
- compiles the questionnaire to a
SurveySchema; -
backend.provision(schema)→BackendConfig; - selects the matching client template (
LocalClientTemplate,SupabaseClientTemplate, orGoogleSheetsClientTemplate); an unknown backend name raisesNotImplementedError; -
builder.build(schema, client=..., env=..., survey=...)→SurveyBundle; -
frontend.publish(bundle, config)→ URL; - returns a populated
DeployResult.
from siamang.deploy import DeployPipeline
from siamang.deploy.backends.local import LocalBackend
from siamang.deploy.frontends.local import LocalFrontend
from siamang.frontend import FrontendBuilder
pipeline = DeployPipeline(
backend=LocalBackend(path="survey.db"),
frontend=LocalFrontend(port=8000),
builder=FrontendBuilder(),
)
result = pipeline.run(survey)Both adapter groups are entry points, so a plugin only declares them and implements the abstract base. Custom backend:
# my_pkg/backend.py
import pandas as pd
from siamang.deploy import BackendAdapter, BackendConfig
class MyBackend(BackendAdapter):
name = "mybackend"
def __init__(self, token: str = ""):
self.token = token
def provision(self, schema) -> BackendConfig:
survey_id = ... # create remote storage, return an id
return BackendConfig(
backend=self.name, survey_id=survey_id,
settings={"ingest_url": "https://api.example.com/ingest"}, # frontend-safe
internal={"token": self.token}, # never bundled
dashboard_url="https://app.example.com/dashboards/...",
)
def get_responses(self, survey_id: str) -> pd.DataFrame: ...
def check_quota(self, survey_id: str, variable, value) -> bool: ...Custom frontend:
# my_pkg/frontend.py
from siamang.deploy import FrontendAdapter
class MyCDNFrontend(FrontendAdapter):
name = "mycdn"
def publish(self, bundle, config) -> str:
bundle.write_to("/tmp/out") # or bundle.to_zip()
return "https://surveys.example.com/abc"Register them, then deploy by name:
# my-siamang-plugin/pyproject.toml
[project.entry-points."siamang.backends"]
mybackend = "my_pkg.backend:MyBackend"
[project.entry-points."siamang.frontends"]
mycdn = "my_pkg.frontend:MyCDNFrontend"survey.deploy(backend="mybackend", frontend="mycdn",
backend_kwargs={"token": "..."})Note: the bundled
DeployPipelineresolves client templates for the three built-in backend names only. A fully custom backend additionally needs a matchingBackendClientTemplateso the frontend knows how to submit; see Frontend and Theming.
See also: Configuration · CLI Reference · Frontend and Theming · Quotas · API Reference Index
siamang · siamang_cloud · Free for noncommercial use · Commercial licensing · Wiki source: wiki/
Getting started
Survey design
- Variables and Measurement
- Question Types
- Pages Blocks and Structure
- Visibility and Branching
- Quotas
- Scripts
Validate & simulate
Data & analysis
Reporting
Frontend & deploy
Tooling
More
Get started
Account & team
Build & deploy
Data & analysis
Author & configure
Reference