Skip to main content

AFFiNE on Google Cloud Run

AFFiNE on Google Cloud Run

AFFiNE is an open-source, privacy-first knowledge base that unifies docs, whiteboards, and databases in one workspace — a self-hostable alternative to Notion and Miro. This module deploys AFFiNE on Cloud Run v2 on top of the App_CloudRun foundation, which provisions and manages the shared Google Cloud infrastructure.

This guide focuses on the cloud services AFFiNE uses and how to explore and operate them from the Google Cloud Console and the command line. For the mechanics common to every Cloud Run application — service identity, ingress and load balancing, scaling and concurrency, CI/CD, Cloud Armor, IAP, Binary Authorization, VPC Service Controls, backups, and the deployment lifecycle — refer to the App_CloudRun foundation guide rather than repeating them here.


What AFFiNE costs on RAD, and how that compares​

AFFiNE on RAD's Cloud Run module costs about US$181 a month in a project you own, with no licence fee for the software itself. RAD charges a one-off module fee of 300 credits (US$30 at the top-up price) in a project you own, and 270 credits (10% lower) in a project RAD manages. If AFFiNE needs to run continuously across pods or beside other Kubernetes workloads, see the GKE guide instead. Figures are as at 8 October 2026; sources are listed at the end of this section.

If you only need it occasionally — studying for a certification, a demo, a seasonal need — a RAD-managed project can be deleted and restored within 30 days for a handful of credits, so that about US$181 a month becomes close to nothing instead. See Pause it for free, below.

What you pay on RAD​

You pay for three things:

In a Google Cloud project you ownIn a project RAD manages for you
Module fee, once per deployment300 credits (US$30 at the top-up price)270 credits (10% lower)
Build timeA few credits, metered per minute of build timeThe same
Google Cloud running costBilled by Google to your own billing account, about US$181 a month for the default configuration (table below)Metered hourly in credits; RAD publishes 83 credits a day, about 2,490 a month (about US$249 at the top-up price, US$199 at the Scale plan's rate)
Who owns whatYou own the project and its billing; RAD deploys and updates itRAD owns the project, with guardrails, quotas and budget alerts; you get console access, and a minimum purchased balance is held in reserve

10 credits cost US$1 on a one-off top-up, and less on a monthly plan. In a project RAD manages, the database and file/cache server are shared by every application in the project, so a second application does not add a second one.

Default running cost in your own project (us-central1, Google list prices):

Resource (module default)Per month
Cloud Run, 2x vCPU / 4 GiB (alwaysOn)US$110.42
Cloud SQL for PostgreSQL, 1 vCPU / 3.75 GB, zonal, 10 GB SSDUS$51.02
NFS/cache file server (small VM)US$13.43
Cloud NAT and networkingUS$5
Cloud Storage (add-ons, backups)US$1
Totalabout US$181

How it compares​

  • Notion, the incumbent AFFiNE is built to replace, charges £8.50/member/month on its Plus plan and £16.50/member/month on Business — a per-seat licence that keeps growing with headcount, against AFFiNE's flat infrastructure cost here.
  • RAD runs the open-source project itself, not a vendor's hosted tenancy. You get the same software, under your own (or RAD's) infrastructure, instead of a per-seat or per-usage subscription that grows independently of what you actually use.
  • Against a bare self-managed server: Running the same open-source software yourself, a Hetzner CPX22 (2 vCPU/4 GB, about $24/month) or a DigitalOcean 2 vCPU/4 GB Droplet (also $24/month) is roughly the same shape; a GCP Compute Engine e2-standard-2 (2 vCPU/8 GB) runs about $49/month. RAD is not cheaper than that in cash terms — the difference is the managed database, Secret Manager and monitoring that come with the module instead of being your own job.

Pause it for free: delete a RAD-managed project, restore it when you need it​

If AFFiNE runs in a project RAD manages for you, you have an option that goes well beyond scaling to zero: delete the whole project, and restore it within 30 days for close to nothing. This suits occasional use — studying for a certification, a demo, a seasonal need — far better than running AFFiNE continuously.

  • How it works. Deleting a RAD-managed project unlinks its billing first, then asks Google to delete the project outright. Google does not remove it immediately: it keeps the project, recoverable, for 30 days, and because billing is already unlinked nothing is charged while it waits. Unlike deleting one module, this does not tear down the database, any VM or the compute resource one by one — the whole project simply stops, and nothing is billed in the meantime.
  • Restoring costs a handful of credits, not a rebuild. Within 30 days, only the project's owner can restore it. RAD asks Google to undelete the project and reattaches its billing account, then asks you to run Update on each deployment to confirm everything came back. Because nothing was individually destroyed, that Update finds the same resources already there — it is a check, not a rebuild, and an Update never charges the module fee again. This costs only a handful of credits in total (under US$1), against paying the module fee and a full build again.
  • What this needs. You must own the project (not one RAD only manages billing for), you must restore it yourself within the 30 days — after that Google deletes it for good — and restoring is admitted like creating a new project, so your purchased credit balance must still clear the tier's floor (100 credits for the sandbox tier most study and demo use fits). Google says most services are fully working again within 36 hours of a restore.
  • One real gap: backups do not survive. Anything AFFiNE writes to a backup bucket inside the project is very likely gone as soon as you delete the project, even though the project itself is recoverable for 30 days — that bucket has Cloud Storage's soft-delete explicitly turned off. Anyone who has customised the deployment and wants to keep that work should copy a backup out (to Google Drive, or a bucket outside the project) before deleting. For a default install with nothing irreplaceable in it, this does not matter.

Pay only while you use it, the other way: delete and redeploy​

This is the fallback for a project you own, or once the 30-day restore window above has passed: delete the deployment, and deploy it again when you next need it.

  • What a redeploy costs. The module fee again, plus the builds: roughly 305 credits (about US$30.50) in your own project, or 275 credits in a RAD-managed one, because RAD rebuilds the resources from scratch. Deleting saves money once AFFiNE would otherwise sit unused long enough to clear that redeploy cost against its own running cost — roughly 5 days or more in your own project (US$6.03/day) or 3 days or more in a RAD-managed one (83 credits/day).
  • Most of the running cost is usually shared infrastructure. The database and file or cache VM stop only when nothing else in the project uses them, so deleting AFFiNE while another application shares the project saves only AFFiNE's own compute part.
  • Keep your data first. Anything AFFiNE writes to a backup bucket inside the deployment is deleted with it — copy the latest backup out (to Google Drive, or a bucket you keep) before deleting if you want to keep it.
  • RAD does not recreate a deleted deployment for you. You enter the settings again when you redeploy.

Lab sessions and Managed Environments​

  • Lab sessions, for training. A trainer runs a session for a class. Each participant gets AFFiNE in their own Google Cloud project for 15 minutes to 24 hours, within an allowance the trainer sets. Either the trainer funds every place, or each participant pays for their own. Everything is deleted when the session ends and unused credits go back to the trainer.
  • Managed Environments, for consultancies. A partner runs AFFiNE for a client from a ring-fenced wallet it funds, and settles with the client directly. If the wallet runs low, billing pauses and the data is kept, so nobody receives an unexpected charge. At the end the partner hands the project over and the deployments become the client's own.

Credits can be bought in more than 20 currencies, including XAF, XOF, NGN, GHS, KES and ZAR, by card, bank transfer or mobile money.

Sources (8 October 2026): Google Cloud list prices from the Cloud Billing Catalog API; RAD's fees and daily-credit estimate from radmodules.dev/pricing; Delete and restore projects for the pause/restore mechanics; Notion pricing. Prices change; check each source before relying on a figure.

1. Overview​

AFFiNE's self-host server runs as a single Node.js container on Cloud Run v2. The deployment wires together a focused set of Google Cloud services:

CapabilityGoogle Cloud serviceNotes
ComputeCloud Run v2Node.js service, 2 vCPU / 4 GiB by default, single always-on instance
DatabaseCloud SQL for PostgreSQL 15Required — AFFiNE does not support MySQL (enforced at plan time)
Real-time collaborationRedisMandatory — Yjs document-sync pub/sub and the job queue; the NFS host co-hosts the default Redis
Blob storageFilestore / NFSUploaded attachments persisted at /root/.affine/storage (gen2 required)
Object storageCloud StorageA dedicated storage bucket provisioned automatically
SecretsSecret ManagerDatabase password managed automatically; AFFiNE needs no app secret
Container imageCloud Build + Artifact RegistryThin custom build over ghcr.io/toeverything/affine
IngressCloud Run URL / Cloud Load BalancingDefault run.app URL, optional external HTTPS LB + custom domain

Sensible defaults worth knowing up front:

  • PostgreSQL 15 is mandatory. A plan-time validation restricts database_type to PostgreSQL versions; MySQL is rejected.
  • Redis is mandatory. A plan-time validation fails the deploy if enable_redis = false. With no explicit redis_host, the NFS server IP is used as the Redis endpoint.
  • Single instance by design. min_instance_count = 1, max_instance_count = 1, cpu_always_allocated = true — real-time collaboration WebSockets must stay reachable and CPU-fed, and per-process collab state plus filesystem blobs make horizontal scaling unsafe.
  • No Cloud SQL socket. enable_cloudsql_volume = false: AFFiNE consumes a URL-authority DATABASE_URL that cannot carry the socket path's colons, so the entrypoint connects over the instance private IP with sslmode=require.
  • Two init jobs on apply. db-init idempotently creates the database and user; affine-migrate runs AFFiNE's self-host-predeploy (schema migration + signing-key generation) before the server starts.
  • No application secret. AFFiNE persists its own signing key in PostgreSQL during migration; only the auto-generated database password lives in Secret Manager.
  • Health probes target / — AFFiNE returns HTTP 200 on its root path once ready.
  • application_version = "latest" maps to stable — AFFiNE publishes no latest image tag.
  • Full-text/vector search is disabled (AFFINE_INDEXER_ENABLED = "false") — the indexer needs a vector backend not provisioned here.

2. Google Cloud Services & How to Explore Them​

All commands assume PROJECT and REGION are set. Service and resource names are reported in the deployment Outputs.

A. Cloud Run — the AFFiNE service​

AFFiNE runs as a Cloud Run v2 service pinned to a single always-on instance. Each deployment creates an immutable revision; traffic moves to the newest healthy one.

  • Console: Cloud Run → select the service for revisions, traffic, logs, and metrics.
  • CLI:
    gcloud run services list --project "$PROJECT" --region "$REGION"
    gcloud run services describe <service-name> --project "$PROJECT" --region "$REGION"
    gcloud run revisions list --service <service-name> --project "$PROJECT" --region "$REGION"

See App_CloudRun for scaling, concurrency, execution environment, and traffic splitting.

B. Cloud SQL for PostgreSQL 15​

AFFiNE stores workspaces, documents, users, and its own signing key in a managed Cloud SQL for PostgreSQL 15 instance. Because AFFiNE's DATABASE_URL is a URL-authority DSN, the service connects over the private IP with TLS (sslmode=require) rather than the Auth Proxy socket. On first deploy the db-init job creates the application database and user, then affine-migrate creates the schema.

  • Console: SQL → select the instance for connections, backups, flags, metrics.
  • CLI:
    gcloud sql instances list --project "$PROJECT"
    gcloud sql instances describe <instance-name> --project "$PROJECT"
    gcloud sql connect <instance-name> --user=<db-user> --project "$PROJECT"

The instance name, database, user, and password secret are in the Outputs. See App_CloudRun for the connection model, backups, and password rotation.

C. Redis — real-time collaboration​

Redis carries AFFiNE's Yjs document-sync pub/sub and its background job queue — the deployment refuses to plan without it. When no external redis_host is configured, the shared NFS server's co-hosted Redis is used automatically.

  • Console: Memorystore → Redis (if using a managed instance); Compute Engine → VM instances (the NFS/Redis host).
  • CLI:
    redis-cli -h <redis-host> ping
    redis-cli -h <redis-host> info clients

D. Filestore (NFS) and Cloud Storage​

Uploaded blobs (images, attachments, file embeds) are written to an NFS share mounted at /root/.affine/storage, so they survive revisions and restarts. A dedicated Cloud Storage bucket (suffix storage) is also provisioned automatically. The gen2 execution environment is required for NFS mounts.

  • Console: Filestore → Instances (or Compute Engine → VM instances for the self-managed NFS VM); Cloud Storage → Buckets.
  • CLI:
    gcloud filestore instances list --project "$PROJECT"
    gcloud storage buckets list --project "$PROJECT"
    gcloud storage ls gs://<storage-bucket>/ # bucket name is in the Outputs

See App_CloudRun for the NFS mount, GCS Fuse, and CMEK.

E. Secret Manager​

The auto-generated database password is the only secret — AFFiNE generates and stores its signing key inside PostgreSQL during the affine-migrate job, so no application secret exists to manage or rotate.

  • Console: Security → Secret Manager.
  • CLI:
    gcloud secrets list --project "$PROJECT"
    gcloud secrets versions access latest --secret=<secret-name> --project "$PROJECT"

See App_CloudRun for injection and rotation details.

F. Networking & ingress​

The service is reachable at its run.app URL by default. An external HTTPS load balancer with a custom domain, Cloud CDN, and Cloud Armor can be layered on; ingress settings and VPC egress control connectivity. The cloud entrypoint defaults AFFINE_SERVER_EXTERNAL_URL to the injected service URL so invites and share links resolve correctly.

  • Console: Cloud Run (service URL); Network services → Load balancing.
  • CLI:
    gcloud run services describe <service-name> --region "$REGION" --format='value(status.url)'
    gcloud compute addresses list --project "$PROJECT"

See App_CloudRun.

G. Cloud Logging & Monitoring​

Container logs flow to Cloud Logging; Cloud Run and Cloud SQL metrics flow to Cloud Monitoring, with optional uptime checks and alert policies.

  • Console: Logging → Logs Explorer; Monitoring → Dashboards / Alerting.
  • CLI:
    gcloud run services logs read <service-name> --project "$PROJECT" --region "$REGION" --limit 50

3. AFFiNE Application Behaviour​

  • Two-stage database setup. On apply, db-init (image postgres:15-alpine) idempotently creates the AFFiNE role and database, grants privileges, and best-effort grants cloudsqlsuperuser so migrations can CREATE EXTENSION. Then affine-migrate runs AFFiNE's node ./scripts/self-host-predeploy using the built app image — idempotent schema migration plus signing-key generation. Both are safe to re-run; affine-migrate retries up to 3 times.
  • Signing key lives in the database. Unlike most apps there is no APP_SECRET-style env var: the key generated by self-host-predeploy is persisted in PostgreSQL, so the deployment carries no application secret to desync or rotate.
  • DSN assembly at startup. The cloud entrypoint builds DATABASE_URL from the Foundation-injected DB_* vars (URL-encoding the credentials) and maps REDIS_HOST/PORT/AUTH to AFFiNE's REDIS_SERVER_*. On Cloud Run it connects to the Cloud SQL private IP with sslmode=require; a preset DATABASE_URL env var takes precedence.
  • External URL. AFFINE_SERVER_EXTERNAL_URL defaults to the Cloud Run service URL. Set it explicitly (via environment_variables) once a custom domain is live so share links and invite emails use the right host.
  • First-run setup. Open the service URL and create the first account — on a fresh AFFiNE self-host instance the first registered user becomes the server administrator, and the admin panel is at <url>/admin.
  • Health path. Startup, liveness, and readiness probes target /, which returns HTTP 200 once the server is ready (startup window: 60 s initial delay + up to 30 × 15 s).
  • Scaling constraint. The service is pinned to exactly one always-on instance. Blobs on the NFS filesystem and per-process Yjs state make multiple instances unsafe; scale vertically (cpu_limit / memory_limit) instead.
  • Verification:
    SERVICE=$(gcloud run services list --project "$PROJECT" --region "$REGION" \
    --filter="metadata.name~affine" --format="value(metadata.name)" --limit=1)
    SERVICE_URL=$(gcloud run services describe "$SERVICE" --project "$PROJECT" \
    --region "$REGION" --format="value(status.url)")
    curl -s -o /dev/null -w "%{http_code}\n" "$SERVICE_URL/" # expect 200

4. Configuration Variables​

Variables are grouped exactly as they appear on the deployment platform. Only settings specific to or notable for AFFiNE are listed; every other input is inherited from App_CloudRun with its standard behaviour.

Group 1 — Project & Identity​

VariableDefaultDescription
project_id(required)Target Google Cloud project.
regionus-central1Region for the service and regional resources.

Group 2 — Deployment Environment​

VariableDefaultDescription
tenant_iddemoShort suffix that makes resource names unique per environment.
support_users[]Emails granted IAM access and monitoring alerts.

All other inputs follow standard App_CloudRun behaviour.

Group 3 — Application Identity​

VariableDefaultDescription
application_nameaffineBase name for resources. Do not change after first deploy.
application_versionstableImage tag for ghcr.io/toeverything/affine; latest maps to stable. Increment to trigger a rebuild and new revision.

All other inputs follow standard App_CloudRun behaviour.

Group 4 — Runtime & Scaling​

VariableDefaultDescription
cpu_limit / memory_limit2000m / 4GiAFFiNE needs at least 2Gi for reliable operation.
container_port3010AFFiNE's native self-host server port.
min_instance_count1Keeps the collaboration WebSocket server always reachable — scale-to-zero drops live editing sessions.
max_instance_count1Pinned. Blobs on the filesystem + per-process collab state make horizontal scaling unsafe.
cpu_always_allocatedtrueWebSocket Yjs sync is starved under request-based CPU throttling — keep true for a live collaborative editor.
enable_cloudsql_volumefalseAFFiNE's URL-authority DATABASE_URL cannot carry the Cloud SQL socket path; the entrypoint uses the private IP with sslmode=require.
container_image_sourcecustomThe thin-wrapper build supplies the cloud entrypoint — required.
execution_environmentgen2Required for the NFS mount.

All other inputs follow standard App_CloudRun behaviour.

Group 6 — Environment Variables & Secrets​

VariableDefaultDescription
environment_variables{}Merged over the Affine_Common defaults (NODE_ENV, AFFINE_SERVER_HOST/PORT, AFFINE_CONFIG_PATH, AFFINE_INDEXER_ENABLED=false). Never set PORT — it is a Cloud Run reserved name and breaks Job creation.
secret_environment_variables{}AFFiNE needs no application secret by default.

All other inputs follow standard App_CloudRun behaviour.

Group 11 — Storage & Filesystem​

VariableDefaultDescription
enable_nfstrueBlob storage and the default Redis host. Keep true unless an external redis_host is supplied.
nfs_mount_path/root/.affine/storageWhere AFFiNE persists uploaded blobs.

All other inputs follow standard App_CloudRun behaviour. The storage GCS bucket is always provisioned by Affine_Common.

Group 12 — Database Backend​

VariableDefaultDescription
database_typePOSTGRES_15AFFiNE requires PostgreSQL — MySQL is rejected at plan time.
db_name / db_useraffine / affineImmutable after first deploy.

All other inputs follow standard App_CloudRun behaviour.

Group 13 — Jobs & Scheduled Tasks​

VariableDefaultDescription
initialization_jobs[]Leave empty for the built-in db-init (postgres:15-alpine) + affine-migrate (built app image) jobs.

All other inputs follow standard App_CloudRun behaviour.

Group 14 — Observability & Health​

VariableDefaultDescription
startup_probeHTTP /, 60 s delay, 30 failuresGenerous first-boot window.
liveness_probeHTTP /, 60 s delayRoot path returns 200 once ready.
uptime_check_configdisabled, path /Cloud Monitoring uptime check.

All other inputs follow standard App_CloudRun behaviour.

Group 21 — Redis​

VariableDefaultDescription
enable_redistrueMandatory — plan-time validation rejects false. Yjs pub/sub + job queue.
redis_host""Leave empty to use the NFS host IP.

All other inputs follow standard App_CloudRun behaviour.

Group 22 — VPC Service Controls​

All inputs follow standard App_CloudRun behaviour (enable_vpc_sc, vpc_cidr_ranges, vpc_sc_dry_run, organization_id, enable_audit_logging).


5. Outputs​

Returned on a successful deployment — the quickest way to locate and explore the running resources.

OutputDescription
service_nameCloud Run service name.
service_urlDefault run.app URL of the service.
service_locationRegion the service runs in.
stage_servicesStage-specific service URLs (Cloud Deploy).
load_balancer_ip / load_balancer_urlExternal HTTPS load balancer IP / URL (when enabled).
database_instance_nameCloud SQL instance name.
database_name / database_userApplication database name / user.
database_password_secretSecret Manager secret holding the DB password.
database_host / database_portDB endpoint / port (host is sensitive).
storage_bucketsCreated Cloud Storage buckets (includes the AFFiNE storage bucket).
network_name / network_exists / regionsVPC network, presence, regions.
container_image / container_registryDeployed image and Artifact Registry repo.
monitoring_enabled / monitoring_notification_channels / uptime_check_namesMonitoring status, channels, uptime checks.
initialization_jobsNames of the setup jobs (db-init, affine-migrate).
deployment_id / tenant_id / resource_prefixNaming identifiers.
project_id / project_numberProject identifiers.
cicd_enabled / github_repository_url / github_repository_owner / github_repository_name / cicd_configurationCI/CD status and details.
artifact_registry_repository / cloudbuild_trigger_name / cloudbuild_trigger_idRegistry and build trigger.
vpc_sc_enabled / vpc_sc_perimeter_name / vpc_sc_dry_run_modeVPC-SC status.
audit_logging_enabled / artifact_registry_cmek_enabledAudit logging and CMEK status.

6. Configuration Pitfalls & Sensible Defaults​

Cross-variable validation runs at plan time (validation.tf): it enforces PostgreSQL, mandatory Redis with a resolvable host, min ≤ max instance counts, and rejects a Cloud SQL volume with database_type = "NONE" — misconfigurations fail fast instead of producing a broken deployment.

Risk: Critical (data loss / outage / security) — High (service degraded) — Medium (cost or partial degradation) — Low (minor).

SettingSensible valueRiskConsequence if wrong
database_typePOSTGRES_15CriticalAFFiNE requires PostgreSQL; MySQL is rejected at plan time.
db_name / db_userset onceCriticalImmutable after first deploy; renaming recreates the DB/user and destroys all workspaces.
enable_redistrueCriticalMandatory — real-time collaboration and the job queue need Redis; false fails the plan.
redis_host"" (NFS) or explicitCriticalRedis on with NFS off and no host set fails validation; a wrong host breaks doc sync at runtime.
enable_nfstrueCriticalWithout NFS, uploaded blobs land on ephemeral disk and vanish on every revision/restart — and the default Redis host disappears.
container_port3010CriticalAFFiNE's native port; a mismatch fails every health probe.
max_instance_count1CriticalMore than one instance splits per-process collab state and filesystem blobs — silent data divergence.
container_image_sourcecustomHighThe upstream image lacks the entrypoint that assembles DATABASE_URL / REDIS_SERVER_* — the server cannot reach its database.
enable_cloudsql_volumefalseHighThe socket path's colons break AFFiNE's URL parser (invalid port); keep private-IP + sslmode=require.
cpu_always_allocatedtrueHighRequest-based throttling starves the Yjs WebSocket sync between requests — live editing stalls.
min_instance_count1HighScale-to-zero drops active collaboration sessions and adds cold-start delays.
memory_limit4Gi (≥ 2Gi)HighNode.js OOM during document sync or migration below 2Gi.
environment_variables PORTnever setHighPORT is Cloud Run-reserved; setting it makes every Job creation fail with HTTP 400.
application_versionstable (pinned tag)MediumNonexistent tags (e.g. literal latest) fail the image build; the module maps latest → stable.
execution_environmentgen2HighNFS mounts require gen2.
AFFINE_SERVER_EXTERNAL_URLservice URL / custom domainMediumWrong host breaks invite links and share URLs.

For the foundation behaviour referenced throughout — service identity, scaling and concurrency, ingress and load balancing, CI/CD, Cloud Armor, IAP, Binary Authorization, VPC-SC, backups, and image mirroring — see App_CloudRun. AFFiNE-specific application configuration shared with the GKE variant is described in Affine_Common.

Need RAD to do something it does not do yet? Request it on the roadmap, or vote on what is already there.