Projects and Services
A Project gathers several Services that ship together, such as a frontend, its API and a background worker, so they can share a repository, environment variables and path routes. Each Service keeps its own builds, environments, domains and deployments; the Project only adds a layer of shared configuration on top.
This feature is experimental and currently in nightly builds only; it is not in a beta or stable release. Names, CLI commands and behavior may change before it ships. See Feature maturity.
Terminology on this page. In this feature, a Service is one deployable app, and a Project is the parent that groups Services. Everywhere else in these docs, and in the published CLI, a single deployable app is still called a "project" (see Projects): that is what this page calls a Service. In the CLI and API the parent is called a project group (project-groups), and the projects command still manages Services.
What a Project is
A Service is one deployable application. A Project is a parent above Services:
- It has a name, a slug, an optional description and a list of Services.
- A Service belongs to at most one Project. Adding a Service to a Project moves it out of the Project it was in.
- A Project is optional. A Service that is in no Project behaves exactly as it does today.
When to use one
Create a Project when Services ship together:
- A monorepo with several deployable apps (web, API, workers) on one branch.
- A frontend that calls a same-origin
/apiserved by another Service. - Several Services that need the same configuration, such as
LOG_LEVELor an external API key. - A multi-service template, such as an app made of a backend, a frontend and a database.
Keep standalone Services when:
- The app is self-contained and shares nothing with other Services.
- You are deploying a single container or static site.
Use a separate Service, not an environment, when the code is a different deployable app (different build, different domain, separate analytics). Use environments within one Service for staging and branch previews of the same app.
What a Project shares
A Project can hold three things for its Services. Each one is optional.
- Name
Repository- Description
One repository and branch for the whole Project, for monorepos. Services that follow the Project's repository build from it, each from its own directory. Changing the Project's repository or branch updates every Service that follows it. A Service can stop following and keep its current settings.
- Name
Shared variables- Description
Variables every Service receives, for example
LOG_LEVELorAPI_BASE_URL. A Service's own variable with the same name wins. See Shared variables and precedence.
- Name
Path routes- Description
Serve a path of one Service's domains from another Service, for example
/apion the frontend's domain from the API Service, so the browser sees one origin. See Path routes.
What stays per Service
Grouping Services does not merge them. Each Service keeps its own:
- builds and deployment history, and whether it deploys on push
- environments, domains and certificates
- framework preset, directory, resources, replicas and health check
- environment variables, which override the Project's shared ones
- linked databases and storage
- analytics, error tracking, logs and monitors
- protections: password, attack mode and limits. A request routed from another Service's domain gets the target Service's protections.
Removing a Service from a Project, or deleting the Project, never stops or deletes a Service. It keeps running as a standalone Service and stops receiving the Project's shared variables on its next deployment. Path routes that involve it are removed.
Shared variables and precedence
A Project's shared variables are one more layer in the environment a deployment receives. For a deployment of a Service, from highest to lowest priority:
- Values Temps sets for the platform, such as
SENTRY_DSN,TEMPS_API_URL,TEMPS_API_TOKENandCRON_SECRET. A Project variable cannot replace them.HOST(default0.0.0.0) is the exception: a Service or Project variable can set it. - The Service's own variables (the project-level and environment-level values from Manage Environment Variables). Where a Service variable and a Project variable with the same name both apply to the environment, the Service's value wins.
- The Project's shared variables. Within the Project, a variable scoped to the environment's slug (for example
production) wins over the Project's all-environments variable with the same name. - Defaults from linked databases and storage, such as
DATABASE_URLfrom a linked PostgreSQL. A Project variable overrides them, so one shared value can point every Service at the same external database.
Values supplied by an operator-configured secrets manager sit above both Service and Project variables, because they override everything except the platform-owned values in step 1.
Rules that apply to Project variables:
- Environment scope. A Project variable is either scoped to one environment slug, which applies only to environments with exactly that slug, or applies to all environments.
- Previews are opt-in. An all-environments Project variable reaches preview environments only when it is marked to include previews, which is off by default. A preview branch therefore never picks up production credentials by accident.
- Names are case-sensitive.
LOG_LEVELandlog_levelare two variables. - Secrets are write-only. A secret Project variable can be replaced but never read back, and its value never appears in error messages.
- Changes apply on the next deployment of each Service. Running containers keep the values they started with.
- A failed decryption fails the deploy. If a Project variable cannot be decrypted, the deployment stops with an error naming the Service, environment, Project and variable key, rather than booting a container without it.
Because the Service's own values win, you can set a default once on the Project and override it for a single Service. See Environment Variables Reference: Project variables for where this layer sits in the full order.
Path routes
A path route sends requests under a prefix, such as /api, on one Service's domains to another Service of the same Project. The frontend calls /api on its own origin and the API Service answers, with no CORS configuration, no build-time API URL and no proxy container.
Both Services must be in the same Project. Each Service keeps its own domains; a route only adds a path to the source Service's domains.
- Name
Every domain of the source- Description
A route belongs to the source Service, not to one hostname. It applies to the environment's default domain, custom domains and preview domains, including domains you add later. It takes effect without a redeploy.
- Name
Environments pair by slug- Description
/apionweb's production goes toapi's production, staging to staging. A preview goes to the target's preview of the same branch. There is no fallback: a preview frontend never calls the production API. When no matching environment exists, requests under the path fail rather than falling back.
- Name
Whole segments, longest prefix wins- Description
/apimatches/apiand/api/users, never/apiary. With routes for/apiand/api/v2,/api/v2/usersgoes to the/api/v2target.
- Name
Keep or strip the prefix- Description
By default
/api/users?page=2reaches the target unchanged. With strip prefix it arrives as/users?page=2and carries anX-Forwarded-Prefix: /apiheader so the target can build links and redirects. A client-suppliedX-Forwarded-Prefixon a routed request is always dropped.
- Name
Reserved paths- Description
Paths the platform answers on every domain cannot be routed:
/_temps,/api/_temps,/api/__temps,/api/oteland/.well-known. A route for/apistill leaves/api/_temps(analytics ingestion) to Temps. A prefix cannot be/.
- Name
The target's protections apply- Description
A routed request gets the target Service's password, attack mode and limits, not the source's. If the frontend is password-protected, protect the API too.
- Name
Cookies are shared- Description
Under one origin, the frontend and the target see the same cookies. That is usually the reason to route, but it means the target also receives the frontend's cookies.
Path routes are designed in upstream ADR-050, which is still marked Proposed.
Monorepo import
When one repository holds several apps (for example apps/web, apps/api and services/worker), you can give the repository to the Project and import each app as a Service:
- The Project holds the repository and branch, either through a git connection or as a public GitHub or GitLab repository.
- Temps scans the branch and lists the apps it detects, with their directory and build preset.
- Each app you pick becomes a Service that follows the Project's repository, building from its own directory.
- Apps that already have a Service (same repository and directory) are marked as already deployed, so importing twice does not create duplicates.
- The import runs on the server one step at a time and resumes from the step that failed, so retrying does not create duplicates.
A public repository is read without a git connection, so pushes to it cannot trigger deploys; deploy each Service when you want it updated, or connect the repository through a git connection.
Path-filtered deploys are not implemented. Without them, a push to a shared repository is queued for every Service that follows it. A design for deploying only the Services whose directories changed is written up as ADR-049 (Proposed), but the merged code does not implement it.
Hosting: apps that run elsewhere
The same feature adds a per-Service hosting setting:
- Hosted: Temps builds and runs the app. This is how every Service works today.
- External: the app runs somewhere else (Vercel, Netlify, your own VPS). Temps never deploys it; deploy, redeploy, rollback and promote are refused, and pushes to a connected repository do not trigger builds. It still has analytics, session replay, error tracking and OpenTelemetry traces.
An External Service can be in a Project like any other, so a Project can hold both the apps Temps runs and the ones it only observes. Hosting is a setting of each Service, not something a Project shares. Switching a Service to External does not stop deployments that are already running.
An External Service does not receive variables from Temps, because Temps never starts its containers. Configure its SDKs with values you copy: an analytics ingest key, the Service's DSN for error tracking and an OpenTelemetry exporter pointed at Temps (see OpenTelemetry).
Multi-service templates
Templates can describe an app made of several Services in one file. Creating such a template from the CLI or console:
- creates a new Project, or uses one you pick,
- creates or links each managed database the template lists,
- adds the template's shared variables to the Project, generating secrets where the template says so,
- creates every Service in the Project, with its own directory, preset, port and resources,
- adds the path routes between them, then deploys.
The template schema gains shared_env_vars, a services list with per-service directory, preset, exposed_port, env_vars and database_bindings, and a routes list with source, prefix, target and strip_prefix:
version: "3"
templates:
- slug: shop
name: Shop
version: 1.0.0
kind: app
databases:
- postgres
shared_env_vars:
- name: SESSION_SECRET
secret: true
default_generator: random_secret
- name: LOG_LEVEL
default: info
services:
- name: backend
directory: backend
preset: dockerfile
exposed_port: 8080
- name: frontend
directory: frontend
preset: vite
routes:
- source: frontend
prefix: /api
target: backend
strip_prefix: false
Key rules the server enforces for multi-service templates:
- A service built from source must pin a fixed git
ref(a tag or commit), becausemainmoves. - A service that runs a prebuilt
imagemust reference it by@sha256:digest. - Every database in
databasesmust be linked to at least one service. - A secret shared variable must use a generator or a value supplied at creation, never a literal default.
- A route's
sourceandtargetmust be two different services of the template, and its prefix cannot be/or a reserved platform path.
Creation is one server-side operation recorded with an id. If it stops or a step fails, you continue the same operation instead of starting again, and steps that already finished are not repeated.
Unified templates are designed in upstream ADR-051, which is still marked Proposed. The exact template schema and the kind values (the old service kind is folded into app) are not final.
CLI examples
These commands are in upstream main and nightly builds but are not in the published @temps-sdk/cli yet; running them against a release without them fails with an unknown command. In the CLI, a Project is managed with project-groups (alias pg) and a Service with projects. The separate services command manages databases and caches, not these.
# Create a Project with two existing Services (slugs or ids from `projects list`)
bunx @temps-sdk/cli project-groups create --name "Acme Shop" --service web --service api
# Add another Service later, and check the result
bunx @temps-sdk/cli project-groups add acme-shop worker
bunx @temps-sdk/cli project-groups show acme-shop
bunx @temps-sdk/cli projects group -p worker
# Shared variables
bunx @temps-sdk/cli project-groups vars set acme-shop LOG_LEVEL info
bunx @temps-sdk/cli project-groups vars set acme-shop API_BASE_URL https://api.example.com -e production
bunx @temps-sdk/cli project-groups vars set acme-shop PAYMENTS_API_KEY --secret
bunx @temps-sdk/cli project-groups vars list acme-shop
bunx @temps-sdk/cli project-groups vars inherited web
# Path routes: /api on web is served by api
bunx @temps-sdk/cli project-groups routes add acme-shop web /api api
bunx @temps-sdk/cli project-groups routes list acme-shop
# Monorepo: set the repository, preview the import, then import
bunx @temps-sdk/cli project-groups repo set acme-shop --repo acme/shop --branch main --connection 3
bunx @temps-sdk/cli project-groups import acme-shop --dry-run
bunx @temps-sdk/cli project-groups import acme-shop
# Follow or retry a template creation or import
bunx @temps-sdk/cli instantiations list --group acme-shop
bunx @temps-sdk/cli instantiations show 42 --wait
bunx @temps-sdk/cli instantiations retry 42
# Hosting
bunx @temps-sdk/cli projects create --external --name marketing-site
bunx @temps-sdk/cli projects hosting external -p marketing-site
# Create an app from a multi-service template
bunx @temps-sdk/cli templates create shop --project "Shop"
The new commands are summarised in the CLI reference: project-groups, instantiations, projects group, projects hosting and templates create. project-groups, instantiations, templates create and projects hosting print a one-time maturity notice before they run.
Limitations and maturity
- Nightly builds only. It is merged upstream but not in a beta or stable release yet, so it carries no stability promise. Naming and inheritance rules may still change.
- Proposed designs. The three design records behind it (ADR-049 monorepo path-filtered deploys, ADR-050 path routes between Services, ADR-051 unified templates) are all marked Proposed. Path-filtered deploys are not implemented.
- One Project per Service. A Service cannot be in two Projects.
- No environment merging. Projects share configuration, not environments: path routes pair environments by slug and shared variables resolve per environment.
- Downgrades. The same change moves service-template snapshots to a new schema version that an older binary cannot read, so downgrading after upgrading to a build that includes it is not supported. Take a backup before trying a nightly build.
Related: Projects, Manage Environment Variables, Environments, Reverse Proxy Routes, Feature Maturity.