From d74f51633626cb90ed7bc691510f6d6db19bb278 Mon Sep 17 00:00:00 2001
From: Nico <47644445+nicotsx@users.noreply.github.com>
Date: Thu, 12 Mar 2026 18:31:42 +0100
Subject: [PATCH] feat: file-based provisionning of volumes and repos (#656)
* feat: file-based provisionning of volumes and repos
docs: provisioning example
chore: ui improvements
* chore: ci issues
---
README.md | 15 +-
app/client/api-client/types.gen.ts | 15 +
app/client/components/managed-badge.tsx | 23 +
.../repositories/routes/edit-repository.tsx | 6 +-
.../repositories/routes/repositories.tsx | 6 +-
app/client/modules/repositories/tabs/info.tsx | 10 +-
.../modules/settings/routes/settings.tsx | 23 +-
app/client/modules/volumes/routes/volumes.tsx | 6 +-
app/client/modules/volumes/tabs/info.tsx | 6 +-
.../20260312141708_loose_gideon/migration.sql | 4 +
.../20260312141708_loose_gideon/snapshot.json | 2271 +++++++++++++++++
app/server/core/config.ts | 2 +
app/server/db/schema.ts | 71 +-
app/server/modules/lifecycle/startup.ts | 7 +
.../modules/provisioning/provisioning.test.ts | 391 +++
.../modules/provisioning/provisioning.ts | 262 ++
.../__tests__/repositories.controller.test.ts | 90 +
.../modules/repositories/repositories.dto.ts | 1 +
.../repositories/repositories.service.ts | 89 +-
.../repositories/repository-config-secrets.ts | 79 +
.../__tests__/volumes.controller.test.ts | 67 +
.../modules/volumes/volume-config-secrets.ts | 47 +
app/server/modules/volumes/volume.dto.ts | 1 +
app/server/modules/volumes/volume.service.ts | 35 +-
app/server/utils/crypto.ts | 104 +-
bun.lock | 1 +
examples/README.md | 2 +-
examples/provisioned-resources/.env.example | 5 +
examples/provisioned-resources/.gitignore | 3 +
examples/provisioned-resources/README.md | 89 +
.../provisioned-resources/docker-compose.yml | 28 +
.../provisioning.example.json | 36 +
.../secrets/aws_secret_access_key.example | 1 +
examples/secrets-placeholders/.env.example | 6 -
examples/secrets-placeholders/.gitignore | 5 -
examples/secrets-placeholders/README.md | 61 -
.../secrets-placeholders/docker-compose.yml | 26 -
37 files changed, 3546 insertions(+), 348 deletions(-)
create mode 100644 app/client/components/managed-badge.tsx
create mode 100644 app/drizzle/20260312141708_loose_gideon/migration.sql
create mode 100644 app/drizzle/20260312141708_loose_gideon/snapshot.json
create mode 100644 app/server/modules/provisioning/provisioning.test.ts
create mode 100644 app/server/modules/provisioning/provisioning.ts
create mode 100644 app/server/modules/repositories/repository-config-secrets.ts
create mode 100644 app/server/modules/volumes/volume-config-secrets.ts
create mode 100644 examples/provisioned-resources/.env.example
create mode 100644 examples/provisioned-resources/.gitignore
create mode 100644 examples/provisioned-resources/README.md
create mode 100644 examples/provisioned-resources/docker-compose.yml
create mode 100644 examples/provisioned-resources/provisioning.example.json
create mode 100644 examples/provisioned-resources/secrets/aws_secret_access_key.example
delete mode 100644 examples/secrets-placeholders/.env.example
delete mode 100644 examples/secrets-placeholders/.gitignore
delete mode 100644 examples/secrets-placeholders/README.md
delete mode 100644 examples/secrets-placeholders/docker-compose.yml
diff --git a/README.md b/README.md
index 5f652a48..e9cdfaa3 100644
--- a/README.md
+++ b/README.md
@@ -100,16 +100,19 @@ Zerobyte can be customized using environment variables. Below are the available
| `LOG_LEVEL` | Logging verbosity. Options: `debug`, `info`, `warn`, `error`. | `info` |
| `SERVER_IDLE_TIMEOUT` | Idle timeout for the server in seconds. | `60` |
| `RCLONE_CONFIG_DIR` | Path to the rclone config directory inside the container. Change this if running as a non-root user. | `/root/.config/rclone` |
+| `PROVISIONING_PATH` | Path to a JSON file with operator-managed repositories and volumes to sync at startup. | (none) |
-### Secret References
+### Provisioned Resources
-For enhanced security, Zerobyte supports dynamic secret resolution for sensitive fields (like passwords, access keys, etc.) in volume and repository configurations. Instead of storing the encrypted secret in the database, you can use one of the following prefixes:
+Zerobyte can sync operator-managed repositories and volumes from a JSON file at startup. This is useful when you want credentials or connection details to live in deployment-time configuration instead of being entered through the UI.
-- `env://VAR_NAME`: Reads the secret from the environment variable `VAR_NAME`.
-- `file://SECRET_NAME`: Reads the secret from `/run/secrets/SECRET_NAME` (standard Docker Secrets path).
+Provisioned resources:
-**Example:**
-When configuring an S3 repository, you can set the Secret Access Key to `env://S3_SECRET_KEY` and then provide that variable in your `docker-compose.yml`.
+- appear in the normal repositories and volumes screens
+- stay read-only in the UI
+- can resolve credential fields from environment variables or `/run/secrets/*` during startup sync
+
+See `examples/provisioned-resources/README.md` for a full example.
### Simplified setup (No remote mounts)
diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts
index c16fdd64..216a4740 100644
--- a/app/client/api-client/types.gen.ts
+++ b/app/client/api-client/types.gen.ts
@@ -337,6 +337,7 @@ export type ListVolumesResponses = {
200: Array<{
id: number;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
status: 'mounted' | 'unmounted' | 'error';
@@ -462,6 +463,7 @@ export type CreateVolumeResponses = {
201: {
id: number;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
status: 'mounted' | 'unmounted' | 'error';
@@ -635,6 +637,7 @@ export type GetVolumeResponses = {
volume: {
id: number;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
status: 'mounted' | 'unmounted' | 'error';
@@ -776,6 +779,7 @@ export type UpdateVolumeResponses = {
200: {
id: number;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
status: 'mounted' | 'unmounted' | 'error';
@@ -986,6 +990,7 @@ export type ListRepositoriesResponses = {
200: Array<{
id: string;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp';
config: {
@@ -1406,6 +1411,7 @@ export type GetRepositoryResponses = {
200: {
id: string;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp';
config: {
@@ -1779,6 +1785,7 @@ export type UpdateRepositoryResponses = {
200: {
id: string;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp';
config: {
@@ -2435,6 +2442,7 @@ export type ListBackupSchedulesResponses = {
volume: {
id: number;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
status: 'mounted' | 'unmounted' | 'error';
@@ -2495,6 +2503,7 @@ export type ListBackupSchedulesResponses = {
repository: {
id: string;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp';
config: {
@@ -2806,6 +2815,7 @@ export type GetBackupScheduleResponses = {
volume: {
id: number;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
status: 'mounted' | 'unmounted' | 'error';
@@ -2866,6 +2876,7 @@ export type GetBackupScheduleResponses = {
repository: {
id: string;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp';
config: {
@@ -3158,6 +3169,7 @@ export type GetBackupScheduleForVolumeResponses = {
volume: {
id: number;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp';
status: 'mounted' | 'unmounted' | 'error';
@@ -3218,6 +3230,7 @@ export type GetBackupScheduleForVolumeResponses = {
repository: {
id: string;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp';
config: {
@@ -3687,6 +3700,7 @@ export type GetScheduleMirrorsResponses = {
repository: {
id: string;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp';
config: {
@@ -3898,6 +3912,7 @@ export type UpdateScheduleMirrorsResponses = {
repository: {
id: string;
shortId: string;
+ provisioningId: string | null;
name: string;
type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp';
config: {
diff --git a/app/client/components/managed-badge.tsx b/app/client/components/managed-badge.tsx
new file mode 100644
index 00000000..c58d178c
--- /dev/null
+++ b/app/client/components/managed-badge.tsx
@@ -0,0 +1,23 @@
+import { Badge } from "~/client/components/ui/badge";
+import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
+
+type ManagedBadgeProps = {
+ label?: string;
+ message?: string;
+};
+
+const defaultMessage =
+ "This resource is provisioned at startup. Changes are useful for testing, but the next provisioning sync can overwrite or recreate it.";
+
+export const ManagedBadge = ({ label = "Managed", message = defaultMessage }: ManagedBadgeProps) => {
+ return (
+ {message}
{repository.provisioningId ? "Provisioned" : "Manual"}
+{repository.compressionMode || "off"}
diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx index 5916d7ab..908fb3a7 100644 --- a/app/client/modules/settings/routes/settings.tsx +++ b/app/client/modules/settings/routes/settings.tsx @@ -1,5 +1,5 @@ import { useMutation } from "@tanstack/react-query"; -import { Download, KeyRound, User, X, Settings as SettingsIcon, Building2 } from "lucide-react"; +import { Download, Fingerprint, KeyRound, User, X, Settings as SettingsIcon, Building2 } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { downloadResticPasswordMutation } from "~/client/api-client/@tanstack/react-query.gen"; @@ -45,7 +45,7 @@ export function SettingsPage({ appContext, initialMembers, initialSsoSettings, i const activeTab = tab || "account"; const navigate = useNavigate(); - const { activeMember } = useOrganizationContext(); + const { activeMember, activeOrganization } = useOrganizationContext(); const isOrgAdmin = activeMember?.role === "owner" || activeMember?.role === "admin"; const handleLogout = async () => { @@ -295,6 +295,25 @@ export function SettingsPage({ appContext, initialMembers, initialSsoSettings, i {isOrgAdmin && (