From 51ca911a45de4fc090334a90d46cadf1b03a0429 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Mon, 7 Sep 2026 11:51:12 +0200 Subject: [PATCH 1/5] feat(templates): add Cal.diy self-hosted scheduling starter Cal.diy is Cal.com's MIT-licensed, self-hosted community edition. Adds it as a project-creation starter (git+Dockerfile build, Postgres service dependency) so operators can self-host booking/scheduling infrastructure on Temps instead of a SaaS like Calendly. Upstream recommends personal/non-production use only, so this ships as a starter template rather than a version-pinned "service" catalog entry. Signed-off-by: David Viejo --- .../templates/starters/cal-diy.yaml | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 crates/temps-core/templates/starters/cal-diy.yaml diff --git a/crates/temps-core/templates/starters/cal-diy.yaml b/crates/temps-core/templates/starters/cal-diy.yaml new file mode 100644 index 000000000..d4378a7d7 --- /dev/null +++ b/crates/temps-core/templates/starters/cal-diy.yaml @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2024-2026 Temps Contributors +# SPDX-License-Identifier: MIT OR Apache-2.0 + +# Cal.diy is Cal.com's MIT-licensed, self-hosted community edition. Upstream +# recommends it for personal/non-production use, not customer-facing booking +# infrastructure -- see https://github.com/calcom/cal.diy. This template is +# for self-hosting it as internal tooling (e.g. an onboarding "book a call" +# step), not as a production dependency. +slug: cal-diy +name: Cal.diy Scheduling +kind: starter +description: Self-hosted scheduling and booking pages -- Cal.com's open-source community edition. MIT-licensed, no license key. Upstream recommends personal/non-production use only. +git: + url: https://github.com/calcom/cal.diy.git + ref: main +preset: dockerfile +exposed_port: 3000 +health_check_path: / +tags: + - scheduling + - booking + - calendar + - self-hosted + - nextjs +features: + - Booking pages, availability rules, and buffers + - Calendar sync with Google, Outlook, and Apple Calendar + - "Team scheduling: round-robin and collective event types" + - Postgres-backed, MIT-licensed, no vendor lock-in +services: + - postgres +env_vars: + - name: DATABASE_URL + description: Postgres connection string. Set this to the POSTGRES_URL value shown on the linked Postgres service. + example: postgresql://user:password@host:5432/calendso + required: true + secret: true + - name: DATABASE_DIRECT_URL + description: Direct (non-pooled) Postgres connection string used for migrations. Same value as DATABASE_URL unless a connection pooler sits in front of Postgres. + example: postgresql://user:password@host:5432/calendso + required: true + secret: true + - name: NEXT_PUBLIC_WEBAPP_URL + description: Public URL this app is served on. + required: true + default_generator: app_url + - name: NEXTAUTH_URL + description: Must equal NEXT_PUBLIC_WEBAPP_URL. + required: true + default_generator: app_url + - name: NEXTAUTH_SECRET + description: Session-signing secret for NextAuth. Generate with `openssl rand -base64 32`. + required: true + secret: true + default_generator: random_secret + - name: CALENDSO_ENCRYPTION_KEY + description: Symmetric key (AES-256, 32 bytes) used to encrypt stored calendar credentials. Generate with `openssl rand -base64 24`. + required: true + secret: true + default_generator: random_secret + - name: EMAIL_FROM + description: From address for booking confirmation and reminder emails. + example: notifications@yourselfhostedcal.example + required: false + - name: EMAIL_SERVER_HOST + description: SMTP server host for outgoing email. + required: false + - name: EMAIL_SERVER_PORT + description: SMTP server port. + example: "587" + required: false +is_public: true +is_featured: false +sort_order: 3 From eda01b0a5c533b565cb0aee97cf5c728b86676e5 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Mon, 7 Sep 2026 17:58:27 +0200 Subject: [PATCH 2/5] fix(templates): move Cal.diy to the curated services catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the Cal.diy template from templates/starters/ to templates/services/ so it appears in the "Services" gallery alongside Keycloak and Browserless, matching the pattern of a curated app backed by a Temps-managed PostgreSQL database. This requires a version-pinned, sha256-digest-locked image, which the previous starter entry avoided for lack of a verified one. docker.io/calendso/calendso publishes real tagged releases (cal.diy's image is still shipped under its pre-rename name) — pinned to v6.2.0, the latest release. Postgres connectivity is wired via managed_service_bindings (DATABASE_URL/DATABASE_DIRECT_URL <- POSTGRES_URL) instead of manual copy-paste env vars, since bindings are only resolved for service-kind templates. Signed-off-by: David Viejo --- .../{starters => services}/cal-diy.yaml | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) rename crates/temps-core/templates/{starters => services}/cal-diy.yaml (56%) diff --git a/crates/temps-core/templates/starters/cal-diy.yaml b/crates/temps-core/templates/services/cal-diy.yaml similarity index 56% rename from crates/temps-core/templates/starters/cal-diy.yaml rename to crates/temps-core/templates/services/cal-diy.yaml index d4378a7d7..c372d2a1b 100644 --- a/crates/temps-core/templates/starters/cal-diy.yaml +++ b/crates/temps-core/templates/services/cal-diy.yaml @@ -3,17 +3,24 @@ # Cal.diy is Cal.com's MIT-licensed, self-hosted community edition. Upstream # recommends it for personal/non-production use, not customer-facing booking -# infrastructure -- see https://github.com/calcom/cal.diy. This template is -# for self-hosting it as internal tooling (e.g. an onboarding "book a call" -# step), not as a production dependency. +# infrastructure -- see https://github.com/calcom/cal.diy. Image is published +# under the project's pre-rename name, docker.io/calendso/calendso, pinned to +# the v6.2.0 release (the latest cal.diy git tag at the time this was added). slug: cal-diy -name: Cal.diy Scheduling -kind: starter -description: Self-hosted scheduling and booking pages -- Cal.com's open-source community edition. MIT-licensed, no license key. Upstream recommends personal/non-production use only. +name: Cal.diy +version: 6.2.0 +kind: service +description: Self-hosted scheduling and booking pages backed by a Temps-managed PostgreSQL database -- Cal.com's open-source community edition. MIT-licensed, no license key. Upstream recommends personal/non-production use only. git: url: https://github.com/calcom/cal.diy.git - ref: main + ref: v6.2.0 preset: dockerfile +image: docker.io/calendso/calendso@sha256:ace3bb1219fb7306585ab9f4d94d41af7ee064c343db0498173436bbe857bd49 +resources: + cpu_request: 500000 + cpu_limit: 1000000 + memory_request: 512 + memory_limit: 1024 exposed_port: 3000 health_check_path: / tags: @@ -21,25 +28,18 @@ tags: - booking - calendar - self-hosted - - nextjs features: - Booking pages, availability rules, and buffers - Calendar sync with Google, Outlook, and Apple Calendar - "Team scheduling: round-robin and collective event types" - - Postgres-backed, MIT-licensed, no vendor lock-in + - PostgreSQL managed, backed up, and restored by Temps services: - postgres +managed_service_bindings: + postgres: + DATABASE_URL: POSTGRES_URL + DATABASE_DIRECT_URL: POSTGRES_URL env_vars: - - name: DATABASE_URL - description: Postgres connection string. Set this to the POSTGRES_URL value shown on the linked Postgres service. - example: postgresql://user:password@host:5432/calendso - required: true - secret: true - - name: DATABASE_DIRECT_URL - description: Direct (non-pooled) Postgres connection string used for migrations. Same value as DATABASE_URL unless a connection pooler sits in front of Postgres. - example: postgresql://user:password@host:5432/calendso - required: true - secret: true - name: NEXT_PUBLIC_WEBAPP_URL description: Public URL this app is served on. required: true @@ -49,12 +49,12 @@ env_vars: required: true default_generator: app_url - name: NEXTAUTH_SECRET - description: Session-signing secret for NextAuth. Generate with `openssl rand -base64 32`. + description: Session-signing secret for NextAuth. required: true secret: true default_generator: random_secret - name: CALENDSO_ENCRYPTION_KEY - description: Symmetric key (AES-256, 32 bytes) used to encrypt stored calendar credentials. Generate with `openssl rand -base64 24`. + description: Symmetric key (AES-256, 32 bytes) used to encrypt stored calendar credentials. required: true secret: true default_generator: random_secret @@ -70,5 +70,5 @@ env_vars: example: "587" required: false is_public: true -is_featured: false -sort_order: 3 +is_featured: true +sort_order: 2 From e3876faf8ee42ffc5155324a1123c92120269a7d Mon Sep 17 00:00:00 2001 From: David Viejo Date: Mon, 7 Sep 2026 18:40:52 +0200 Subject: [PATCH 3/5] feat(templates): support per-architecture pinned images for service templates Some upstream projects don't publish a real multi-arch manifest under one tag/digest -- amd64 and arm64 ship as separate, separately-versioned images instead. cal-diy's v6.2.0 image (docker.io/calendso/calendso) is one of these: it 404s pulling on arm64 hosts because the pinned digest only ever resolves to the amd64 variant. Adds an optional `image_arm64` field to service templates (same pinned-digest validation as `image`), exposes it through the API, and resolves between the two at project-creation time based on the host architecture. This assumes a homogeneous fleet since the deploy target's architecture isn't known until scheduling, after project creation -- true per-node arch-aware resolution would need to move into the deploy path, which is out of scope here. cal-diy.yaml now pins both variants: amd64 -> v6.2.0, arm64 -> v6.2.0-arm. Signed-off-by: David Viejo --- crates/temps-core/src/templates.rs | 21 +++++ .../templates/services/cal-diy.yaml | 4 + .../temps-projects/src/handlers/handlers.rs | 77 ++++++++++++++++++- .../temps-projects/src/handlers/templates.rs | 6 ++ 4 files changed, 105 insertions(+), 3 deletions(-) diff --git a/crates/temps-core/src/templates.rs b/crates/temps-core/src/templates.rs index 16e7a587c..a3996b12b 100644 --- a/crates/temps-core/src/templates.rs +++ b/crates/temps-core/src/templates.rs @@ -189,6 +189,16 @@ pub struct ProjectTemplate { /// absent, the template builds from source. #[serde(default)] pub image: Option, + /// arm64/aarch64 variant of `image`, used instead of it when the deploy + /// host is arm64. Populate this when the upstream registry does not + /// publish a real multi-arch manifest under one tag/digest (some + /// self-hosted projects ship amd64 and arm64 as entirely separate, + /// separately-tagged images instead of one manifest list) -- `image` + /// alone would then only ever resolve to one architecture regardless of + /// where it is deployed. Must be an immutable `@sha256:` digest, same as + /// `image`, when `kind` is `service`. + #[serde(default)] + pub image_arm64: Option, /// Optional command passed to the container image. This is needed for /// production images whose default command is intentionally a development /// mode (for example Keycloak). @@ -776,6 +786,16 @@ impl TemplateService { "Service templates must use an immutable sha256 image digest".to_string(), ); } + if template + .image_arm64 + .as_deref() + .is_some_and(|image| !image.trim().is_empty() && !is_pinned_image_reference(image)) + { + errors.push( + "Service template image_arm64 must use an immutable sha256 image digest" + .to_string(), + ); + } if template .exposed_port .is_none_or(|port| !(1..=65_535).contains(&port)) @@ -1397,6 +1417,7 @@ templates: preset: "nextjs".to_string(), preset_config: None, image: None, + image_arm64: None, command: None, resources: None, exposed_port: None, diff --git a/crates/temps-core/templates/services/cal-diy.yaml b/crates/temps-core/templates/services/cal-diy.yaml index c372d2a1b..b31a98051 100644 --- a/crates/temps-core/templates/services/cal-diy.yaml +++ b/crates/temps-core/templates/services/cal-diy.yaml @@ -6,6 +6,9 @@ # infrastructure -- see https://github.com/calcom/cal.diy. Image is published # under the project's pre-rename name, docker.io/calendso/calendso, pinned to # the v6.2.0 release (the latest cal.diy git tag at the time this was added). +# calendso/calendso does not publish a real multi-arch manifest under one +# tag/digest -- amd64 and arm64 are separate, separately-versioned tags +# (v6.2.0 vs v6.2.0-arm) -- hence the separate image_arm64 pin below. slug: cal-diy name: Cal.diy version: 6.2.0 @@ -16,6 +19,7 @@ git: ref: v6.2.0 preset: dockerfile image: docker.io/calendso/calendso@sha256:ace3bb1219fb7306585ab9f4d94d41af7ee064c343db0498173436bbe857bd49 +image_arm64: docker.io/calendso/calendso@sha256:4b0fa72eec13bd3ddb608a6d13f05bf0ebc136e73832abfe1a8ec145db9e4651 resources: cpu_request: 500000 cpu_limit: 1000000 diff --git a/crates/temps-projects/src/handlers/handlers.rs b/crates/temps-projects/src/handlers/handlers.rs index 9e5f1f128..d742eb8e7 100644 --- a/crates/temps-projects/src/handlers/handlers.rs +++ b/crates/temps-projects/src/handlers/handlers.rs @@ -2968,6 +2968,30 @@ fn is_pinned_sha256_image_reference(image: &str) -> bool { }) } +/// Best-effort choice between `image` and `image_arm64` for the architecture +/// this process is running on. Project creation happens before deployment +/// scheduling, so the eventual target node's architecture isn't known yet -- +/// this assumes a homogeneous fleet (the console host's own architecture), +/// which matches the documented single/few-node self-hosted reference +/// deployment. A heterogeneous multi-node fleet with mixed architectures is +/// not resolved correctly by this heuristic. +fn select_template_image_for_arch<'a>( + template: &'a temps_core::templates::ProjectTemplate, + default_image: &'a str, + host_arch: &str, +) -> &'a str { + if host_arch == "aarch64" { + if let Some(arm_image) = template + .image_arm64 + .as_deref() + .filter(|image| !image.is_empty()) + { + return arm_image; + } + } + default_image +} + fn resolve_image_template_runtime( template: &temps_core::templates::ProjectTemplate, request: &super::templates::CreateProjectFromTemplateRequest, @@ -2979,7 +3003,13 @@ fn resolve_image_template_runtime( return Ok(None); }; - let image_ref = request.image.as_deref().unwrap_or(template_image).trim(); + let image_ref = request + .image + .as_deref() + .unwrap_or_else(|| { + select_template_image_for_arch(template, template_image, std::env::consts::ARCH) + }) + .trim(); if image_ref.is_empty() { return Err(TemplateRuntimeOverrideError::InvalidImage { reason: "the image reference cannot be empty".to_string(), @@ -3884,8 +3914,9 @@ mod tests { parse_owner_repo_from_git_url, production_environment_variable_names, project_created_from_template_telemetry_event, require_git_settings_permissions, require_template_creation_permissions, resolve_image_template_runtime, - service_template_changes, validate_template_service_selection, DropPresetCandidate, - TemplateEnvironmentError, TemplateRuntimeOverrideError, TemplateServiceSelectionError, + select_template_image_for_arch, service_template_changes, + validate_template_service_selection, DropPresetCandidate, TemplateEnvironmentError, + TemplateRuntimeOverrideError, TemplateServiceSelectionError, }; use axum::http::StatusCode; use chrono::Utc; @@ -4111,6 +4142,46 @@ mod tests { assert_eq!(stored["imageRuntime"]["healthCheckPath"], "/realms/master"); } + #[test] + fn arch_selection_prefers_image_arm64_only_on_arm64_hosts() { + let template = temps_core::templates::bundled_template_by_slug("cal-diy") + .expect("Cal.diy should be bundled"); + let default_image = template.image.as_deref().expect("Cal.diy image"); + let arm_image = template + .image_arm64 + .as_deref() + .expect("Cal.diy image_arm64"); + assert_ne!( + default_image, arm_image, + "fixture should exercise two distinct pinned images" + ); + + assert_eq!( + select_template_image_for_arch(&template, default_image, "aarch64"), + arm_image + ); + assert_eq!( + select_template_image_for_arch(&template, default_image, "x86_64"), + default_image + ); + } + + #[test] + fn arch_selection_falls_back_to_default_image_without_an_arm64_pin() { + let template = temps_core::templates::bundled_template_by_slug("keycloak") + .expect("Keycloak should be bundled"); + assert!( + template.image_arm64.is_none(), + "fixture should exercise a template with no arm64 override (its image is already multi-arch)" + ); + let default_image = template.image.as_deref().expect("Keycloak image"); + + assert_eq!( + select_template_image_for_arch(&template, default_image, "aarch64"), + default_image + ); + } + #[test] fn image_template_runtime_applies_user_overrides_and_can_clear_command() { let template = temps_core::templates::bundled_template_by_slug("keycloak") diff --git a/crates/temps-projects/src/handlers/templates.rs b/crates/temps-projects/src/handlers/templates.rs index d627493f2..bbe4248c5 100644 --- a/crates/temps-projects/src/handlers/templates.rs +++ b/crates/temps-projects/src/handlers/templates.rs @@ -55,6 +55,10 @@ pub struct TemplateResponse { /// Prebuilt Docker image reference. When set, the one-click deploy pulls and /// runs this image directly (no build); when absent it builds from `git`. pub image: Option, + /// arm64 variant of `image`, used instead of it when the deploy host is + /// arm64. Present only when the upstream image does not publish a real + /// multi-arch manifest under `image`'s tag/digest. + pub image_arm64: Option, /// Optional command passed to the image entrypoint. pub command: Option>, /// Curated CPU/memory profile applied when the project is created. @@ -125,6 +129,7 @@ impl From for TemplateResponse { .collect(), is_featured: template.is_featured, image: template.image, + image_arm64: template.image_arm64, command: template.command, resources: template.resources, exposed_port: template.exposed_port, @@ -308,6 +313,7 @@ mod tests { preset_config: None, resources: None, image: None, + image_arm64: None, command: None, exposed_port: None, health_check_path: None, From 7d30c40312dd13e7b68cfc46357ae01c3f15be8e Mon Sep 17 00:00:00 2001 From: David Viejo Date: Mon, 7 Sep 2026 19:38:50 +0200 Subject: [PATCH 4/5] fix(templates): resolve per-arch image before the create-project form sees it The create-project form always submits template.image as an explicit runtime override, even when the user never edited it -- so the arch-selection fallback added in resolve_image_template_runtime never actually ran for a real UI-driven deploy: by the time the request arrives, an untouched default is indistinguishable from a deliberate override. Resolves image vs image_arm64 by host architecture in the two template-serving endpoints (list_project_templates, get_project_template) instead, so the value the form is pre-filled with -- and therefore submits -- is already correct. Signed-off-by: David Viejo --- .../temps-projects/src/handlers/handlers.rs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/temps-projects/src/handlers/handlers.rs b/crates/temps-projects/src/handlers/handlers.rs index d742eb8e7..fea895ee6 100644 --- a/crates/temps-projects/src/handlers/handlers.rs +++ b/crates/temps-projects/src/handlers/handlers.rs @@ -2748,6 +2748,7 @@ pub async fn list_project_templates( let response = super::templates::ListTemplatesResponse { templates: templates .into_iter() + .map(resolve_template_for_host_arch) .map(super::templates::TemplateResponse::from) .collect(), total, @@ -2789,7 +2790,9 @@ pub async fn get_project_template( .with_title("Template Not Found") .with_detail(e.to_string()) })?; - Ok(Json(super::templates::TemplateResponse::from(template))) + Ok(Json(super::templates::TemplateResponse::from( + resolve_template_for_host_arch(template), + ))) } /// List all available template tags @@ -2975,6 +2978,24 @@ fn is_pinned_sha256_image_reference(image: &str) -> bool { /// which matches the documented single/few-node self-hosted reference /// deployment. A heterogeneous multi-node fleet with mixed architectures is /// not resolved correctly by this heuristic. +/// Overwrite `image` with the host-architecture-appropriate variant before a +/// template is shown to a client. The create-project form always submits +/// whatever `image` it was shown as an explicit runtime override (even when +/// the user never touched it), so arch selection has to happen here -- by +/// the time a request reaches `resolve_image_template_runtime`, an unedited +/// default already looks identical to a deliberate override. +fn resolve_template_for_host_arch( + mut template: temps_core::templates::ProjectTemplate, +) -> temps_core::templates::ProjectTemplate { + if let Some(default_image) = template.image.as_deref().filter(|image| !image.is_empty()) { + let resolved = + select_template_image_for_arch(&template, default_image, std::env::consts::ARCH) + .to_string(); + template.image = Some(resolved); + } + template +} + fn select_template_image_for_arch<'a>( template: &'a temps_core::templates::ProjectTemplate, default_image: &'a str, From e6303bedce3531ce817fc6798cebf682c29ea861 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Mon, 7 Sep 2026 19:48:01 +0200 Subject: [PATCH 5/5] fix(templates): pin Cal.diy to calcom/cal.com instead of calendso/calendso Same release build, same digests (verified: identical sha256 for both v6.2.0 and v6.2.0-arm) -- calcom/cal.com is just a better-known alias for the same push. calcom/cal.diy, the name matching the project's own git repo, has no tags published to it as of this writing, so it can't be used. Signed-off-by: David Viejo --- .../templates/services/cal-diy.yaml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/temps-core/templates/services/cal-diy.yaml b/crates/temps-core/templates/services/cal-diy.yaml index b31a98051..fdf1a6b58 100644 --- a/crates/temps-core/templates/services/cal-diy.yaml +++ b/crates/temps-core/templates/services/cal-diy.yaml @@ -3,12 +3,15 @@ # Cal.diy is Cal.com's MIT-licensed, self-hosted community edition. Upstream # recommends it for personal/non-production use, not customer-facing booking -# infrastructure -- see https://github.com/calcom/cal.diy. Image is published -# under the project's pre-rename name, docker.io/calendso/calendso, pinned to -# the v6.2.0 release (the latest cal.diy git tag at the time this was added). -# calendso/calendso does not publish a real multi-arch manifest under one -# tag/digest -- amd64 and arm64 are separate, separately-versioned tags -# (v6.2.0 vs v6.2.0-arm) -- hence the separate image_arm64 pin below. +# infrastructure -- see https://github.com/calcom/cal.diy. The same +# release-docker.yaml CI build pushes this image to several Docker Hub +# namespaces (calcom/cal.com, calcom/cal.diy, calendso/calendso); only +# calcom/cal.com and calendso/calendso actually have published tags +# (calcom/cal.diy has none as of this writing) -- pinned here to v6.2.0, +# the latest cal.diy git tag at the time this was added. It does not publish +# a real multi-arch manifest under one tag/digest -- amd64 and arm64 are +# separate, separately-versioned tags (v6.2.0 vs v6.2.0-arm) -- hence the +# separate image_arm64 pin below. slug: cal-diy name: Cal.diy version: 6.2.0 @@ -18,8 +21,8 @@ git: url: https://github.com/calcom/cal.diy.git ref: v6.2.0 preset: dockerfile -image: docker.io/calendso/calendso@sha256:ace3bb1219fb7306585ab9f4d94d41af7ee064c343db0498173436bbe857bd49 -image_arm64: docker.io/calendso/calendso@sha256:4b0fa72eec13bd3ddb608a6d13f05bf0ebc136e73832abfe1a8ec145db9e4651 +image: docker.io/calcom/cal.com@sha256:ace3bb1219fb7306585ab9f4d94d41af7ee064c343db0498173436bbe857bd49 +image_arm64: docker.io/calcom/cal.com@sha256:4b0fa72eec13bd3ddb608a6d13f05bf0ebc136e73832abfe1a8ec145db9e4651 resources: cpu_request: 500000 cpu_limit: 1000000