Managed services
The catalog and its providers, bound and standalone services, connecting, TLS, browser hosts, backups.
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.
| Provider | What | Versions | Settings | Secret |
|---|---|---|---|---|
postgres | PostgreSQL | postgresVersion: 18, 17 | database, user (both app) | password, 12 to 128 characters |
mariadb Since 1.3.0 | MariaDB | mariadbVersion: 12.3, 11.8 (default), 11.4 | database, user (both app) | password, 12 to 128 characters |
valkey Since 1.3.0 | Valkey (Redis-compatible): a cache or job queue | valkeyVersion: 9.1 | user (app), persistence (rdb, aof, none), maxmemoryPolicy | password, 16 to 128 characters |
rabbitmq Since 1.3.0 | RabbitMQ: a message broker | rabbitmqVersion: 4.3 | vhost, user (both app), defaultQueueType (quorum, classic) | password, 12 to 128 characters |
s3 Since 1.3.0 | S3 object storage on SeaweedFS | none: one release per Harthelm release | buckets (default data), publicBuckets, bucketQuotaMiB | secretAccessKey, 32 to 128 characters |
Plans, as CPU / memory / storage; tiny is the default:
| Provider | tiny | small | medium |
|---|---|---|---|
postgres | 0.25 / 256 MiB / 1 GiB | 0.5 / 512 MiB / 5 GiB | |
mariadb | 0.25 / 384 MiB / 1 GiB | 0.5 / 768 MiB / 5 GiB | 1 / 2 GiB / 20 GiB |
valkey | 0.1 / 128 MiB / 512 MiB | 0.25 / 512 MiB / 2 GiB | 0.5 / 1 GiB / 4 GiB |
rabbitmq | 0.25 / 512 MiB / 1 GiB | 0.5 / 1 GiB / 5 GiB | 1 / 2 GiB / 10 GiB |
s3 | 0.25 / 256 MiB / 2 GiB | 0.5 / 512 MiB / 10 GiB | 1 / 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: nonekeeps 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
jdbcUrlfor 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.
publicBucketsare readable without a key;bucketQuotaMiBcaps each bucket, and a bucket over its quota refuses writes. Use path-style requests; the region isus-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 landscape | Standalone | |
|---|---|---|
| Created by | a type: managed service in harthelm.yml and a Sync | the catalog's Choose, or hart ms create |
| Changed by | harthelm.yml and a Sync | the console's Configuration tab |
| Its secret | in the workspace's vault; Sync asks for it | shown once after it is created or rotated |
| Paused | by the workspace's Teardown; the next Sync resumes it | by Pause and Resume |
| Deleted | when you remove it from the file and Sync | by 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:
| Template | Value |
|---|---|
${{ 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.0 | the secret alone: the password, or S3's secret access key |
${{ services.db.details.<field> }} | another field the provider reports |
| Provider | Port | dsn | Useful details |
|---|---|---|---|
postgres | 5432 | postgresql://<user>:<password>@<host>:5432/<database> | |
mariadb | 3306 | mysql://<user>:<password>@<host>:3306/<database> | jdbcUrl, without credentials |
valkey | 6379 | redis://<user>:<password>@<host>:6379/0 | maxmemory |
rabbitmq | 5672 | amqp://<user>:<password>@<host>:5672/<vhost> | managementUrl (http://<host>:15672/, inside the cluster) |
s3 | 8333 | the endpoint http://<host>:8333, without the secret | accessKeyId, 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:
| Provider | With TLS |
|---|---|
valkey | rediss://<user>:<password>@<host>:6380/0 |
rabbitmq | amqps://<user>:<password>@<host>:5671/<vhost>, the management API at https://<host>:15671/ |
mariadb | the same port 3306; TLS is offered, not required |
s3 | the 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:
| Broker | Signed in at once | Other team members |
|---|---|---|
| Standalone | every member of the team | |
| Bound to a landscape | editors and admins of its workspace | reach 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:
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.
| Works | Does not work |
|---|---|
| A presigned URL opened as a page: a download, a link your app shows | fetch 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 in | the 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 key | anonymous 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:
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:
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.
| Provider | What a backup holds |
|---|---|
postgres, mariadb | the database, as an SQL dump |
valkey | a snapshot of every key, whatever the persistence |
rabbitmq | the topology: virtual host, users, queues, exchanges, bindings, policies; not the messages |
s3 | every 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.