Everything is set through environment variables, all of them prefixed APPSWEET_. Three of
them are required before the process will start at all: APPSWEET_DATABASE_URL,
APPSWEET_OIDC_ISSUER, and APPSWEET_OIDC_CLIENT_ID. The process environment wins over a
.env file sitting next to the binary.
Server
| Variable | Requirement | Description |
|---|---|---|
APPSWEET_DATABASE_URL |
required to boot | Postgres connection string. Appsweet runs its migrations automatically on boot, and the first of them needs the vector extension — see Docker Compose. |
APPSWEET_DATABASE_MAX_CONNECTIONS |
default 5 | Size of the Postgres connection pool. |
APPSWEET_BIND_ADDR |
default 127.0.0.1:3000 |
Socket address the gateway listens on. Inside a container this has to be 0.0.0.0:3000, or nothing outside reaches it. |
APPSWEET_PUBLIC_BASE_URL |
default http://localhost:3000 |
The URL browsers reach Appsweet on. Sign-in redirect URIs are derived from it. |
APPSWEET_TRUST_X_FORWARDED |
default false |
Set to true only behind a proxy you control that sets the forwarding headers itself. |
APPSWEET_CORS_ALLOWED_ORIGINS |
optional | Comma-separated browser origins allowed to call the REST API directly. The defaults cover the desktop client. |
APPSWEET_BACKEND_PROCESS_MODE |
default serve+worker |
Whether this process runs the HTTP gateway, the background worker, or both. The default is what makes one container a complete deployment; serve and worker exist for splitting them across replicas. |
Sign-in
Appsweet does not manage passwords itself — authentication is OIDC, and the bundled stack runs Zitadel for it.
Both variables below are read while the process is starting up, not lazily on the first
sign-in attempt. A container given only a database URL exits before it binds a port, so
/ready never answers at all. This is a boot requirement, not a feature you can defer.
| Variable | Requirement | Description |
|---|---|---|
APPSWEET_OIDC_ISSUER |
required to boot | Absolute http(s) URL of the Zitadel instance. Discovery is derived from it. |
APPSWEET_OIDC_CLIENT_ID |
required to boot | Client ID Appsweet presents to the issuer. |
The issuer has to be Zitadel. These two variables take any OIDC issuer URL and the
backend will complete a sign-in against it, but the workspace it then looks up comes from
Zitadel's own claims: the authorization request asks for the scope
urn:zitadel:iam:user:resourceowner, and provisioning reads
urn:zitadel:iam:user:resourceowner:id, …:name and …:primary_domain from the token.
Without them /v1/me returns 403 workspace_claims_required and the client never gets a
workspace. Mapping another provider's claims onto these is not implemented, so Keycloak,
Authentik, Auth0 and Entra ID do not work today.
File storage
Uploads live in S3-compatible object storage, not on a local disk — there is no data directory to mount. Storage is required for any real deployment: the storage layer is always S3-protocol, with a configured endpoint, credentials, and bucket names.
The bundled default is Garage, which the repository's own stack brings up for you, but nothing depends on it. Any S3-compatible endpoint works — Hetzner Object Storage, Cloudflare R2, Backblaze B2, AWS S3, Wasabi, or your own Garage or SeaweedFS.
Leaving APPSWEET_S3_ENDPOINT unset does not stop the backend booting: it starts with files
disabled and everything else working, and says nothing about it in the logs. That is a
development convenience for trying Appsweet out, not a supported storage-less mode.
Anything other people will use needs storage configured.
| Variable | Requirement | Description |
|---|---|---|
APPSWEET_S3_ENDPOINT |
required | Endpoint the backend uses for its own S3 operations. Unset disables files — development only. |
APPSWEET_S3_PUBLIC_ENDPOINT |
optional | Endpoint presigned URLs are minted against, for when clients cannot reach the internal one. |
APPSWEET_S3_ACCESS_KEY |
required | S3 access key. |
APPSWEET_S3_SECRET_KEY |
required | S3 secret key. |
APPSWEET_S3_REGION |
default garage |
S3 region. |
APPSWEET_S3_UPLOADS_BUCKET |
default uploads |
Bucket for user-uploaded files. |
APPSWEET_S3_BACKUPS_BUCKET |
default backups |
Bucket for backups, kept separate so it can carry its own retention. |
APPSWEET_S3_DERIVED_BUCKET |
default derived |
Bucket for regenerable artefacts such as thumbnails. |
APPSWEET_S3_PRESIGN_TTL_SECS |
default 900 | Lifetime of a presigned upload or download URL, in seconds. |
Create the buckets yourself
Naming a bucket here does not create it. Appsweet's storage layer only ever issues object
operations — put, get, head, delete, presign — and never CreateBucket. All three buckets
have to exist before the first file operation, and the key in APPSWEET_S3_ACCESS_KEY needs
read and write on each of them.
Nothing tells you at boot. With APPSWEET_S3_ENDPOINT set the backend comes up with files
enabled and reports itself ready; the missing bucket only surfaces when someone uploads or
downloads something, as an error from the storage provider.
That is exactly the work the bundled stack does for you: it runs a one-shot garage-init
service that waits for Garage, then creates $APPSWEET_S3_UPLOADS_BUCKET,
$APPSWEET_S3_BACKUPS_BUCKET and $APPSWEET_S3_DERIVED_BUCKET and grants
$APPSWEET_S3_ACCESS_KEY read, write and owner on each. On your own S3 provider, do the
equivalent by hand or in your provisioning before you point Appsweet at it.
Sweetheart
There is nothing here to configure. Sweetheart is designed to run on-device inside the desktop app, and the backend reads no AI-related environment variables at all. Sweetheart and models explains what that means and what changes later.