Harthelm Docs
Concepts

Managed services

The catalog and its providers, bound and standalone services, connecting, TLS, browser hosts, backups.

Last updated 2026-10-19

A managed service is a database, a cache, a message broker or an object store that Harthelm runs for your team: you say what you want, Harthelm creates it, keeps its secret, connects your services to it and backs it up every night.

The catalog

Managed services in the console's navigation has two tabs: the team's Managed services and the Catalog. The catalog lists the providers your installation offers, each with its versions, plans, settings and what it can do (pause, nightly backups, rotate secrets). hart lists the team's services with hart ms list.

Harthelm has five built-in providers. Your administrator may turn some off or offer more providers and plans.

ProviderWhatVersionsSettingsSecret
postgresPostgreSQLpostgresVersion: 18, 17database, user (both app)password, 12 to 128 characters
mariadb Since 1.3.0MariaDBmariadbVersion: 12.3, 11.8 (default), 11.4database, user (both app)password, 12 to 128 characters
valkey Since 1.3.0Valkey (Redis-compatible): a cache or job queuevalkeyVersion: 9.1user (app), persistence (rdb, aof, none), maxmemoryPolicypassword, 16 to 128 characters
rabbitmq Since 1.3.0RabbitMQ: a message brokerrabbitmqVersion: 4.3vhost, user (both app), defaultQueueType (quorum, classic)password, 12 to 128 characters
s3 Since 1.3.0S3 object storage on SeaweedFSnone: one release per Harthelm releasebuckets (default data), publicBuckets, bucketQuotaMiBsecretAccessKey, 32 to 128 characters

Plans, as CPU / memory / storage; tiny is the default:

Providertinysmallmedium
postgres0.25 / 256 MiB / 1 GiB0.5 / 512 MiB / 5 GiB
mariadb0.25 / 384 MiB / 1 GiB0.5 / 768 MiB / 5 GiB1 / 2 GiB / 20 GiB
valkey0.1 / 128 MiB / 512 MiB0.25 / 512 MiB / 2 GiB0.5 / 1 GiB / 4 GiB
rabbitmq0.25 / 512 MiB / 1 GiB0.5 / 1 GiB / 5 GiB1 / 2 GiB / 10 GiB
s30.25 / 256 MiB / 2 GiB0.5 / 512 MiB / 10 GiB1 / 1 GiB / 50 GiB

Every instance is a single server, without replicas: a restart, a Rotate or a node drain interrupts it for a few seconds (S3 for about 20). What else to know per provider:

  • Valkey keeps half of its plan's memory for data. persistence: none keeps everything in memory only: a restart or a pause empties it, and the console warns before.
  • MariaDB can move to a newer series later (11.4 to 11.8 or 12.3, 11.8 to 12.3), upgraded in place; it never goes back, so dump it first. Its details carry a jdbcUrl for Java.
  • RabbitMQ: durable queues, such as quorum queues (the default), and persistent messages survive a restart; transient messages do not.
  • S3 object storage creates the buckets you list (up to 20). A bucket you remove from the list keeps its objects. publicBuckets are readable without a key; bucketQuotaMiB caps each bucket, and a bucket over its quota refuses writes. Use path-style requests; the region is us-east-1. Bucket names follow S3's rules: 3 to 63 characters, no two dots in a row, no IP address, no name twice, and every public bucket is one of the buckets; a name that breaks them is refused when you create or change the store Since 1.4.0.

Bound and standalone

A managed service lives in one of two ways:

Bound to a landscapeStandalone
Created bya type: managed service in harthelm.yml and a Syncthe catalog's Choose, or hart ms create
Changed byharthelm.yml and a Syncthe console's Configuration tab
Its secretin the workspace's vault; Sync asks for itshown once after it is created or rotated
Pausedby the workspace's Teardown; the next Sync resumes itby Pause and Resume
Deletedwhen you remove it from the file and Syncby Delete or hart ms delete, typing its name

A bound service is the usual choice: it comes and goes with the landscape, and your services read its address from templates. See Add a database.

Connecting

Other services of the same harthelm.yml read a bound service's connection in their env:

TemplateValue
${{ services.db.host }}the host
${{ services.db.port }}the port
${{ services.db.dsn }}a connection URL with the secret, e.g. postgresql://app:<password>@<host>:5432/app
${{ services.db.secret }} Since 1.3.0the secret alone: the password, or S3's secret access key
${{ services.db.details.<field> }}another field the provider reports
ProviderPortdsnUseful details
postgres5432postgresql://<user>:<password>@<host>:5432/<database>
mariadb3306mysql://<user>:<password>@<host>:3306/<database>jdbcUrl, without credentials
valkey6379redis://<user>:<password>@<host>:6379/0maxmemory
rabbitmq5672amqp://<user>:<password>@<host>:5672/<vhost>managementUrl (http://<host>:15672/, inside the cluster)
s38333the endpoint http://<host>:8333, without the secretaccessKeyId, region, buckets, browserEndpoint (https://ms-<n>.<workspace domain>, for browsers) Since 1.4.0

They are filled in when the service starts. When they change, for example after a Rotate, the services that use them restart once the new value works. Use ${{ services.db.secret }} rather than the vault key (${{ secrets.<key> }}) for the secret alone: the vault key keeps the value of your last Sync, so after a Rotate the service would hold the old one until you sync again.

A standalone service's host and port are in its details; use them like any other address.

In-cluster TLS

Where your installation has in-cluster TLS and cert-manager, Valkey, RabbitMQ and MariaDB Since 1.3.0 and S3 Since 1.4.0 get a certificate of the installation's internal CA. The connection URLs above stay plain and work as before. An app that wants TLS uses:

ProviderWith TLS
valkeyrediss://<user>:<password>@<host>:6380/0
rabbitmqamqps://<user>:<password>@<host>:5671/<vhost>, the management API at https://<host>:15671/
mariadbthe same port 3306; TLS is offered, not required
s3the endpoint https://<host>:8443

and trusts the internal CA, the file $HART_INTERNAL_CA_FILE in the editor and in services (see Workspace environment); the AWS SDKs read it from AWS_CA_BUNDLE. The detail tls says whether the instance has the TLS listeners. A renewal, about every 75 days, restarts the server for a few seconds (S3 for about 20 to 35).

From a browser

Managed services are reachable inside your team's namespace only. For browsers, RabbitMQ and S3 get a host of their own Since 1.4.0:

https://ms-<n>.<workspace domain>

<n> is the instance's number; it is never given to another instance, also not after a delete. The details drawer shows the host on its Overview under Browser host, and the API in the service's browser.url. PostgreSQL, MariaDB and Valkey have no host: run an admin tool as a service of your landscape instead. Your administrator can switch the hosts off; then use the forward routes below.

RabbitMQ's management UI

Open management UI opens RabbitMQ's management UI in a new tab, already signed in as you: with the tags management and policymaker on the broker's virtual host, for one hour. After that, the UI's Click here to log in signs you in again through the console. The broker's own user and password still work at the UI's login form and for AMQP clients.

The host is always behind your team's sign-in, like a private dev URL, and it is never public. Who gets the single sign-on:

BrokerSigned in at onceOther team members
Standaloneevery member of the team
Bound to a landscapeeditors and admins of its workspacereach the UI's login form and sign in with the broker's user and the password from the workspace's vault

Scripts call RabbitMQ's HTTP API on the host with an API key in Harthelm-Authorization and the broker's user and password.

S3 from browsers

Presign URLs with the store's host, the detail browserEndpoint, and path-style addressing:

harthelm.yml
  web:
    env:
      S3_ENDPOINT: ${{ services.files.dsn }}                                # in the cluster: http://<host>:8333
      S3_PUBLIC_ENDPOINT: ${{ services.files.details.browserEndpoint }}     # for browsers: https://ms-42.<workspace domain>

The host passes path and query unchanged, so a URL presigned with browserEndpoint works for every key, also for keys with spaces, +, % or non-ASCII characters.

Behind the sign-in (the default), only members of your team reach the host. Open the store's host in the drawer, or opening a presigned URL in a fresh browser, signs the browser in for 12 hours.

WorksDoes not work
A presigned URL opened as a page: a download, a link your app showsfetch or XHR from a dev host to the store: such requests carry no cookie and get 401
<img>, <video>, <audio> and download links on a page of your team's dev hosts, once the browser is signed inthe same in the console's Preview, on a custom domain, or under a workspace domain on the Public Suffix List
Signed requests from scripts that add Harthelm-Authorization with an API keyanonymous use of presigned URLs: that is what a public host is for

Add Harthelm-Authorization after your S3 client has signed the request. Harthelm removes the header before the store sees it, so a signature over it fails with SignatureDoesNotMatch. Hook it in where the SDK hands over the signed request: botocore's before-send event, a middleware of the AWS SDK for JavaScript v3 after its signing step, a http.RoundTripper around the client's transport in Go. Presigned URLs are not affected.

Public. Public host in the drawer switches the host public, after a confirmation that names what becomes reachable. A public host has no sign-in: anyone can read the objects of the store's publicBuckets (no listings), and every other request needs a signature with the store's key, so a presigned URL works for anyone who holds it until it expires. The switch takes effect within a second and restarts nothing. Every member of the team may switch it; for a store bound to a landscape, the editors and admins of its workspace. harthelm.yml has no key for it, and a Sync never changes it. With hart: hart ms create s3 … --public; with the API: "public": true in PATCH /managed-services/{ms}. Your administrator may forbid public hosts; then the switch is disabled and says so, and a store switched public before is served behind the sign-in again until they are allowed.

Pages on your dev hosts may send presigned requests, uploads included, to a public host: the store's CORS rule allows the origins https://*.<workspace domain>. Since 1.5.0 a page on one of your team's custom domains may do the same: the router answers for an Origin that is exactly https://<name> of a custom domain of the store's own team that is verified and has not lapsed (a restricted domain counts, it is the team's too). It answers a preflight itself, with GET, HEAD, PUT, POST, DELETE, the requested header names and a cache time of 300 seconds, and it adds the origin and the exposed headers (ETag, which a multipart upload needs, and the x-amz ids and checksums) to the answer. It never sends Access-Control-Allow-Credentials and never *. Nothing restarts when a domain comes or goes: the routers follow within a second, and a domain that is removed, lapses or moves to another team gets nothing from the next request on (a browser that cached a preflight may send requests for up to 300 seconds more; the store's signature check decides those as for any client). Another team's domain, an unverified one and any other origin get no CORS answer. Gated hosts get no CORS for custom domains (a page on another site sends no cookie of the host, so the gate answers its preflight with 401), and neither does RabbitMQ's management host. A paused store's preflight is still answered, but its requests get the paused page without CORS headers, so the script sees a CORS error. Nothing is needed in harthelm.yml.

Forward routes

Without the hosts, or for script uploads from a dev host during development, route through a forward service on your dev domain, behind your workspace's sign-in. A forward target names the object behind the service: ws-<workspace id>-<name> for a bound one, ms-<name> for a standalone one, in the team's namespace rg-<team id>.

RabbitMQ's management UI, for a bound broker mq, at https://<dev domain>/mq/ (with the trailing slash); sign in with the broker's user and password:

harthelm.yml
  mq-ui:
    type: forward
    target: http://ws-${{ workspace.id }}-mq.rg-${{ team.id }}.svc:15672/
    routes: [{path: /mq, strip: true}]

S3: route each bucket's path to the store as it is, without strip, and presign with https://${{ workspace.devDomain }} as the endpoint. For a bound store files with the buckets uploads and assets:

harthelm.yml
  s3:
    type: forward
    target: http://ws-${{ workspace.id }}-files.rg-${{ team.id }}.svc:8333
    routes: [{path: /uploads}, {path: /assets}]

The requests are same-origin with your app, so its sign-in cookie goes along. An app's own routes must not take a bucket's name. Behind a route with a prefix and strip (such as /s3), only keys of letters, digits and - . _ ~ / work.

What can change later

The provider and its version are a service's identity: another provider is a new service, which starts empty. Some settings are fixed once the service exists (such as postgresVersion, a database's user, Valkey's persistence), versions may only move up (MariaDB to a newer series, Valkey and RabbitMQ to a later minor version), and storage can only grow. Where your installation's storage class cannot grow volumes, a plan that needs more storage is refused. Sync and the console check these rules and say what they refuse.

Backups

Managed services are backed up every night unless you turn it off (backups: false, or Nightly backups off). The details drawer shows the last dump and has Dump now. A paused service cannot be dumped.

ProviderWhat a backup holds
postgres, mariadbthe database, as an SQL dump
valkeya snapshot of every key, whatever the persistence
rabbitmqthe topology: virtual host, users, queues, exchanges, bindings, policies; not the messages
s3every object of every bucket; a night reads only the objects that changed

Restoring a backup is a task for your administrator (Backups and restore).

Removing a managed service deletes its data

Removing or renaming a bound service in harthelm.yml deletes its data at the next Sync; the review warns before. Deleting a standalone one does the same at once. The last nightly backup is then the only copy.

Sizes and quota

A managed service counts against the team's quota at its whole plan, CPU and memory, because its server reserves all of it Since 1.3.0. A paused one counts nothing. See Plans and quotas.

Next steps

On this page