Service Bindings
Service bindings are used to give applications access to endpoint credentials. Postgres, MySQL and SQLite databases are currently supported by OpenRun. For Postgres and MySQL, the administrator creates a service with connection information for the database. Apps can easily get access to an isolated database/schema without any manual configuration being required. OpenRun uses the admin credentials to create binding accounts for applications. Apps can share access to a schema, with support for granting limited permissions across applications. For SQLite, there is no external database: the binding gives the app a persistent volume holding its SQLite database files, with optional continuous replication to S3-compatible storage.
Service bindings are an easy way to configure one database installation properly (with backups, fault tolerance, security etc) and then safely share that database across multiple apps. This is an alternate approach as against usual deployment tooling where each app is assumed to create its own database from scratch, which ignores the challenges with ensuring that the database is properly administered.
The currently supported service types are:
| Service type | Purpose |
|---|---|
postgres | Create Postgres schemas and roles |
mysql | Create MySQL databases and users |
sqlite | Provide a persistent volume with SQLite database files per app |
Concepts
A Service is an admin connection to an endpoint (database). Apps do not use this connection directly. OpenRun uses the service connection to create database users and apply grants.
A Base Binding is created from a service. It creates the main database account for an app. For Postgres, this creates a schema and role. For MySQL, this creates a database and user.
A Derived Binding is created from a base binding. It uses the same schema or database as the base binding, but gets a separate account. Grants on the derived binding control what that derived account can do.
Bindings always have a staging and a prod environment. Grant related binding changes are applied to the staged binding first. Use binding update --promote to promote the staged grants to prod. Staging apps are bound to the stage binding env and prod apps are bound to the prod env. This gives an easy way to ensure that the staging app has access to an isolated test environment which is very similar to the prod env.
Create Services
Use openrun service create to create a service. The service id is <service_type>/<service_name>.
openrun service create postgres/main \
--is-default \
--config url=postgres://admin:secret@db.example.com:5432/appdb
openrun service create mysql/main \
--is-default \
--config url=mysql://admin:secret@db.example.com:3306/The first service of a type is automatically marked as default. Use --is-default to explicitly mark a service as the default. When creating a binding, the source can be the full service id like postgres/main, or just the service type like postgres. If only the service type is specified, OpenRun uses the default service for that type.
Service config values can reference secrets with {{secret ...}} or {{secret_from ...}} template references, so credentials do not have to be stored in the metadata database:
openrun service create postgres/main \
--config 'url={{secret_from "asm" "prod_db_admin_url"}}'The reference is stored as is and resolved through the secret provider each time the service connection is used, so a rotated secret value takes effect on the next operation without a service update.
Under RBAC, creating or updating a service whose config references a secret additionally requires the secret:read permission: the referenced value flows resolved to the service’s binding provider, so selecting which secrets a service uses is restricted to users allowed to use the secret store, rather than implied by service:manage alone.
openrun binding create postgres /apps/reporting-dbList services with:
openrun service list
openrun service list postgres
openrun service list postgres/mainUpdate service default status with:
openrun service update postgres/main --set-default=trueDelete a service with:
openrun service delete postgres/mainHostname Mapping Basics
Service connection URLs are used in two different places: OpenRun uses the admin URL to create and manage binding accounts, and app containers use generated account URLs to connect at runtime. These hostnames can be different.
Generated binding accounts include both url and url_direct. url is intended for app containers and can replace the service URL hostname using the service config key binding_hostname. url_direct keeps the original service URL hostname and is used by OpenRun commands such as binding run-command.
This matters for local database services. If a Postgres or MySQL service URL points at localhost or 127.0.0.1, app containers usually cannot use that hostname to reach the host database. Outside Kubernetes, OpenRun automatically maps those local hostnames to a container-reachable hostname. Docker uses host.docker.internal; other local container runtimes use host.containers.internal. Set binding_hostname explicitly to override that value, or set binding_hostname=disable to keep the service URL hostname unchanged.
Staging Services
A service can specify a separate staging service. The staging service has to be of the same service type. When a binding is created, OpenRun creates the staged account using the staging service and the prod account using the main service. Create the staging service first, then reference it from the main service:
openrun service create postgres/stage \
--config url=postgres://admin:secret@stage-db.example.com:5432/appdb
openrun service create postgres/main \
--is-default \
--staging stage \
--config url=postgres://admin:secret@prod-db.example.com:5432/appdbYou can add, change, or clear the staging service later:
openrun service update postgres/main --staging stage
openrun service update postgres/main --staging ""The staging service cannot refer to itself. If no staging service is linked, then stage bindings are created on the same endpoint as the prod, just a separate schema/database. Stage performance issues can impact prod in that case.
Create Base Bindings
Create a base binding using a service source:
openrun binding create postgres/main /apps/reporting-db
openrun binding create mysql/main /apps/inventory-dbBase bindings cannot have grants. The generated account owns the bindings schema or database.
The account information is not shown by binding get or binding list. Use
binding show-account to view the generated connection information. With RBAC
enabled, show-account needs the binding:reveal permission, which requires
an explicit grant (it is not implied by binding:manage and binding owners do
not hold it by default).
openrun binding show-account /apps/reporting-db
openrun binding show-account --staging /apps/reporting-dbFor testing, SQL can be run as the binding account. Output can be truncated for large results sets.
openrun binding run-command /apps/reporting-db "select current_user"
openrun binding run-command --staging /apps/reporting-db "select current_user"Create Derived Bindings
Create a derived binding by using a base binding path as the source.
openrun binding create --grant "read:*" /apps/reporting-db /apps/reporting-read
openrun binding create --grant "create:*" /apps/reporting-db /apps/reporting-writer
openrun binding create --grant "full:events" /apps/reporting-db /apps/reporting-events-adminDerived bindings have to be created from base bindings. A derived binding cannot be used as the source for another derived binding.
Grants are supported only on derived bindings. A grant is specified as type:target.
| Grant | Meaning |
|---|---|
read:* | Read all tables |
read:<table> | Read one table. If the table does not exist yet, the grant is deferred. |
create:* | Create tables |
full:* | Read, write and create |
full:<table> | Read and write one table |
create:<table> is not supported. Create access applies to the schema or database.
If a table-specific grant references a table which does not exist yet, the grant is kept in the metadata and will be applied later on next update call or using --reapply-all.
Update and Promote Grants
Grant updates are staged. The update is applied to the staged account first.
openrun binding update --add-grant "read:*" /apps/reporting-read
openrun binding update --delete-grant "read:old_table" /apps/reporting-readProd is not updated until the binding is promoted:
openrun binding update --promote /apps/reporting-readYou can update and promote in one command:
openrun binding update \
--add-grant "read:*" \
--delete-grant "read:old_table" \
--promote \
/apps/reporting-readUse --reapply-all to apply all grants again. This is useful after creating a table for which a table-specific grant was previously deferred, or after manual database changes.
openrun binding update --reapply-all --promote /apps/reporting-readBinding promotion is separate from app promotion. app promote promotes the app version and app metadata, including the list of binding paths attached to the app. It does not promote staged grant changes inside a binding. Use binding update --promote or apply --promote for that.
Attach Bindings to Apps
Attach existing bindings when creating an app:
openrun app create \
--bind /apps/reporting-db \
github.com/example/reporting-app \
/reportingBinding order is preserved. To update the binding list for an existing app:
openrun app update bindings /apps/reporting-read /apps/metrics-read /reportingThis updates staging. Add --promote to update prod in the same command.
Binding Access Control
Access to services and bindings is controlled through RBAC. The service:* and binding:* permissions are scoped by the grant’s service:<glob> / binding:<glob> target entries. Attaching a binding to an app requires the binding:use permission on that binding path (or service:bind on the service, for auto bindings created from a service source), in addition to the app permission for the update itself. The creator of a service or binding holds the owner permissions (service:manage / binding:manage by default) on their own entries.
When RBAC is not enabled, management operations are restricted to the admin user, so no separate approval step applies to binding access.
Auto Bindings
When the value passed to --bind starts with /, OpenRun treats it as an existing binding path. When it does not start with /, OpenRun treats it as a service source and creates a base binding automatically if the binding does not already exist.
openrun app create \
--bind postgres/main \
--approve \
github.com/example/reporting-app \
/reportingThe generated binding is stored under:
/auto/<main-app-id>/<service-type>For example, a Postgres auto binding is stored as /auto/app_prd_.../postgres. Duplicate service references in the same command resolve to one auto binding.
The /auto path is reserved for auto bindings. Users cannot create bindings under that path directly. A derived binding can use an auto binding path as its source.
Binding config can be passed through the service source using ; followed by comma separated key=value entries. The params become the auto binding’s config, exactly like binding create --config:
openrun app create \
--bind "sqlite;path=/mydata" \
--approve \
github.com/example/notes-app \
/notesThis works for any service type. The config is used when the auto binding is first created; referencing an existing auto binding with a different config is an error (delete the auto binding to recreate it with new config).
Declarative Apply
Apply files can define bindings using the binding builtin.
binding("/apps/reporting-db", "postgres/main", config={"inherit_default": "false"})
binding("/apps/reporting-read", "/apps/reporting-db", grants=["read:*"])
app("/reporting", "github.com/example/reporting-app", bindings=["/apps/reporting-read"])The builtin format is:
binding(path, source, grants=[], config={})| Property | Optional | Type | Default | Notes |
|---|---|---|---|---|
| path | False | string | The unique path for the binding | |
| source | False | string | The source for binding, service or based binding path | |
| grants | true | string array | The permission grants for a derived binding | |
| config | true | dict | The config map |
The source rules are the same as the CLI:
source="postgres/main"orsource="postgres"creates a base binding.source="/apps/reporting-db"creates a derived binding.grantsis valid only for derived bindings.configis used only when the binding is first created.
openrun apply creates bindings even if the app glob does not match any apps. Existing binding sources and binding config cannot be changed.
For existing bindings, apply does a three-way merge for grants. Grant changes in the apply file are applied, and grant changes made using the CLI are preserved. Use --clobber to make the staged grants match the apply file.
openrun apply --reload=none apps.ace /reporting
openrun apply --promote --reload=none apps.ace /reportingWith --promote, apply promotes binding metadata after updating staged metadata.
Postgres Config and Behavior
Postgres services require url. They also support binding_hostname.
| Key | Required | Description |
|---|---|---|
url | Yes | Admin Postgres connection URL |
binding_hostname | No | Hostname to substitute into generated url account URLs. url_direct keeps the original service URL hostname. If omitted for a localhost or 127.0.0.1 service URL outside Kubernetes, OpenRun automatically uses a container-reachable host name. Set to disable to keep url unchanged and skip automatic mapping |
For example:
openrun service create postgres/main \
--config url=postgres://admin:secret@localhost:5432/appdbThe admin user in the URL must be able to create roles, create schemas, grant privileges and alter default privileges.
Postgres bindings support one create-time binding config key:
| Key | Default | Description |
|---|---|---|
inherit_default | true | Whether the generated role inherits privileges from other roles, including PUBLIC |
For example:
openrun binding create \
--config inherit_default=false \
postgres/main \
/apps/reporting-dbIf inherit_default is set to false, the generated role is created with NOINHERIT.
For a base binding, OpenRun creates a schema and a login role. The generated account includes url and url_direct. url uses the service URL with the generated username and password, replacing the hostname with binding_hostname when that service option is set. If binding_hostname is omitted, the service URL hostname is localhost or 127.0.0.1, and OpenRun is not running in Kubernetes mode, OpenRun automatically uses host.docker.internal for Docker and host.containers.internal for other local container runtimes. Set binding_hostname=disable to opt out of both explicit hostname substitution and automatic mapping. url_direct uses the original service URL hostname. Containers receive both values as environment variables, for example POSTGRES_URL and POSTGRES_URL_DIRECT. binding run-command uses url_direct. OpenRun sets the generated role’s default search_path to the binding schema.
For a derived binding, OpenRun creates a separate login role and uses the base binding schema. The derived role gets USAGE on the schema before grants are applied.
Postgres grants work as follows:
read:*grantsSELECTon all current tables and changes default privileges so future tables created by the base role are readable by the derived role.create:*grantsCREATEon the schema.full:*grants all table privileges, all sequence privileges andCREATEon the schema. Default privileges are also updated for future tables and sequences.read:<table>andfull:<table>apply only to the specified table.
If a table-specific grant references a table which does not exist, OpenRun skips the grant for that run. Skipped grants are applied on the next update/apply run.
MySQL Config and Behavior
MySQL services require url. They also support host_pattern and binding_hostname.
| Key | Required | Description |
|---|---|---|
url | Yes | Admin MySQL URL |
host_pattern | No | Host part for generated MySQL users. Defaults to % |
binding_hostname | No | Hostname to substitute into generated url account URLs. url_direct keeps the original service URL hostname. If omitted for a localhost or 127.0.0.1 service URL outside Kubernetes, OpenRun automatically uses a container-reachable host name. Set to disable to keep url unchanged and skip automatic mapping |
For example:
openrun service create mysql/main \
--config url=mysql://admin:secret@localhost:3306/ \
--config host_pattern=10.0.%The admin user in the URL must be able to create users, create databases, and grant and revoke privileges.
For a base binding, OpenRun creates a database and user. The base user gets ALL PRIVILEGES on the generated database. The generated account includes url and url_direct; url uses binding_hostname when that service option is set. If binding_hostname is omitted, the service URL hostname is localhost or 127.0.0.1, and OpenRun is not running in Kubernetes mode, OpenRun automatically uses host.docker.internal for Docker and host.containers.internal for other local container runtimes. Set binding_hostname=disable to opt out of both explicit hostname substitution and automatic mapping. url_direct keeps the original service URL hostname. Containers receive both values as environment variables, for example MYSQL_URL and MYSQL_URL_DIRECT. binding run-command uses url_direct.
For a derived binding, OpenRun creates a separate user and uses the base binding database. The derived user gets a minimal database-level SHOW VIEW grant so it can connect using the generated database as the default database.
MySQL grants work as follows:
read:*grantsSELECTon the database. This applies to current and future tables.create:*grantsCREATE,ALTER,INDEX,DROP, andREFERENCESon the database.full:*grants read, write, create, alter, index, drop, references, trigger, create view, temporary table and lock privileges on the database.read:<table>andfull:<table>apply only to the specified table.
If a table-specific grant references a table which does not exist, OpenRun skips the grant for that run. Skipped grants are applied on the next update/apply run.
MySQL DDL statements auto-commit. If binding creation fails part way through, OpenRun does best-effort cleanup for users and databases created during that operation.
SQLite Config and Behavior
The sqlite service type has no external endpoint. A SQLite binding gives the app a persistent named volume (Docker/Podman) or PersistentVolumeClaim (Kubernetes) mounted into the app container, holding the app’s SQLite database files. Creating the service and binding needs no connection information:
openrun service create sqlite/main --is-default
openrun app create --bind sqlite --approve github.com/example/notes-app /notesThe app finds its database through environment variables:
SQLITE_URL=file:/data/data.db
SQLITE_DB_PATH=/data/data.db
SQLITE_DIR=/dataThe volume is created automatically when the app first starts and is reused across app updates and redeploys. The volume identity follows the binding, so attaching the binding to a different app later does not carry over another binding’s data. On Kubernetes, apps with a SQLite binding automatically run as a single replica with the Recreate deploy strategy, matching SQLite’s single-writer model.
SQLite services support these config keys (all optional):
| Key | Description |
|---|---|
litestream_config | Name of a [litestream.<name>] server config entry. Enables continuous replication to S3-compatible storage for bindings of this service |
path_prefix | Overrides the litestream config’s replica key prefix for bindings of this service |
volume_size | Kubernetes PVC size (default kubernetes.default_volume_size, 10Gi). Ignored for Docker/Podman |
SQLite bindings support these create-time binding config keys:
| Key | Default | Description |
|---|---|---|
path | /data | Absolute path where the volume is mounted in the container |
pattern | *.db | File glob (relative to the binding directory) selecting which files are replicated when replication is enabled |
openrun binding create --config path=/mydata sqlite/main /apps/notes-db
openrun binding create --config "pattern=*.sqlite3" sqlite/main /apps/notes-dbApps can create additional database files under SQLITE_DIR (for example per-tenant databases); with replication enabled, every file matching the binding’s pattern (default *.db) in the directory is replicated. The default database file the binding’s environment variables point at is data.db, so a custom pattern should either match data.db or the app should use its own file names.
Differences from Postgres/MySQL bindings:
- An app can have at most one SQLite binding, and a SQLite binding can be attached to only one app: the database is a single-writer file on a per-app volume.
- Derived bindings and grants are not supported. SQLite has no accounts or roles to scope; bind the base binding directly.
binding run-commandis not supported: the database file is only reachable inside the app container.binding show-accountshows the computed paths; there are no credentials.- SQLite bindings are not available for preview apps.
- Deleting a binding or app keeps the volume and any replicated data, consistent with the other service types.
A staging service can be linked like any other service type. Staged apps then follow the staging service’s config: its own litestream_config (or none), path_prefix and volume_size, so staged data can replicate to a separate location or skip replication entirely.
For best results the app should open the database in WAL mode with a busy timeout, for example file:...?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000) (driver dependent). With replication enabled, WAL mode is required, but litestream enables it automatically if the app has not.
OpenRun makes the volume writable for non-root app users automatically. With replication enabled, the chmod runs using the Litestream image, so any app image works. For local-only SQLite bindings (no litestream_config), the chmod runs with the app’s own image, so the app image must not be distroless. On Kubernetes, pods with a SQLite binding additionally get fsGroup: 65532, which makes the volume group-writable at mount time on storage classes with ownership management.