diff --git a/AGENTS.md b/AGENTS.md
index a1c72922..290f3239 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -8,7 +8,7 @@
## Project Overview
-Zerobyte is a backup automation tool built on top of Restic that provides a web interface for scheduling, managing, and monitoring encrypted backups. It supports multiple volume backends (NFS, SMB, WebDAV, SFTP, local directories) and repository backends (S3, Azure, GCS, local, and rclone-based storage).
+Zerobyte is a backup automation tool built on top of Restic that provides a web interface for scheduling, managing, and monitoring encrypted backups. It supports multiple volume backends (NFS, SMB, WebDAV, SFTP, local directories) and repository backends (S3, B2, Azure, GCS, local, and rclone-based storage).
### Type Checking
diff --git a/README.md b/README.md
index 514fe9d7..f0105089 100644
--- a/README.md
+++ b/README.md
@@ -233,6 +233,7 @@ A repository is where your backups will be securely stored encrypted. Zerobyte s
- **S3-compatible storage** - Amazon S3, MinIO, Wasabi, DigitalOcean Spaces, etc.
- **Google Cloud Storage** - Google's cloud storage service
- **Azure Blob Storage** - Microsoft Azure storage
+- [**Backblaze B2**](https://www.backblaze.com/cloud-storage) - S3 compatible cloud storage
- **rclone remotes** - 40+ cloud storage providers via rclone (see below)
Repositories are optimized for storage efficiency and data integrity, leveraging Restic's deduplication and encryption features.
diff --git a/app/client/modules/repositories/components/create-repository-form.tsx b/app/client/modules/repositories/components/create-repository-form.tsx
index 54b0ffc3..d3e755ec 100644
--- a/app/client/modules/repositories/components/create-repository-form.tsx
+++ b/app/client/modules/repositories/components/create-repository-form.tsx
@@ -26,6 +26,7 @@ import {
gcsRepositoryConfigSchema,
localRepositoryConfigSchema,
r2RepositoryConfigSchema,
+ b2RepositoryConfigSchema,
rcloneRepositoryConfigSchema,
restRepositoryConfigSchema,
s3RepositoryConfigSchema,
@@ -46,6 +47,7 @@ import {
import { useServerFn } from "@tanstack/react-start";
import { getServerConstants } from "~/server/lib/functions/server-constants";
import { useSuspenseQuery } from "@tanstack/react-query";
+import { B2RepositoryForm } from "./repository-forms/b2-repository-form";
const formBaseFields = {
name: z.string().min(2).max(32),
@@ -56,6 +58,7 @@ export const formSchema = z
.discriminatedUnion("backend", [
localRepositoryConfigSchema.extend(formBaseFields),
s3RepositoryConfigSchema.extend(formBaseFields),
+ b2RepositoryConfigSchema.extend(formBaseFields),
r2RepositoryConfigSchema.extend(formBaseFields),
gcsRepositoryConfigSchema.extend(formBaseFields),
azureRepositoryConfigSchema.extend(formBaseFields),
@@ -87,6 +90,7 @@ type Props = {
const defaultValuesForType = (repoBase: string) => ({
local: { backend: "local" as const, compressionMode: "auto" as const, path: repoBase },
s3: { backend: "s3" as const, compressionMode: "auto" as const },
+ b2: { backend: "b2" as const, compressionMode: "auto" as const },
r2: { backend: "r2" as const, compressionMode: "auto" as const },
gcs: { backend: "gcs" as const, compressionMode: "auto" as const },
azure: { backend: "azure" as const, compressionMode: "auto" as const },
@@ -185,6 +189,7 @@ export const CreateRepositoryForm = ({
Local
S3
Cloudflare R2
+ Backblaze B2
Google Cloud Storage
Azure Blob Storage
REST Server
@@ -313,6 +318,7 @@ export const CreateRepositoryForm = ({
{watchedBackend === "local" && }
{watchedBackend === "s3" && }
+ {watchedBackend === "b2" && }
{watchedBackend === "r2" && }
{watchedBackend === "gcs" && }
{watchedBackend === "azure" && }
diff --git a/app/client/modules/repositories/components/repository-forms/b2-repository-form.tsx b/app/client/modules/repositories/components/repository-forms/b2-repository-form.tsx
new file mode 100644
index 00000000..ba45e4a6
--- /dev/null
+++ b/app/client/modules/repositories/components/repository-forms/b2-repository-form.tsx
@@ -0,0 +1,79 @@
+import type { UseFormReturn } from "react-hook-form";
+import {
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "../../../../components/ui/form";
+import { Input } from "../../../../components/ui/input";
+import { SecretInput } from "../../../../components/ui/secret-input";
+import type { RepositoryFormValues } from "../create-repository-form";
+
+type Props = {
+ form: UseFormReturn;
+};
+
+export const B2RepositoryForm = ({ form }: Props) => {
+ return (
+ <>
+ (
+
+ Bucket
+
+
+
+ Backblaze bucket name for storing backups.
+
+
+ )}
+ />
+ (
+
+ Bucket
+
+
+
+ Path inside the bucket to store data
+
+
+ )}
+ />
+ (
+
+ Access Key ID
+
+
+
+ S3 access key ID for authentication.
+
+
+ )}
+ />
+ (
+
+ Secret Access Key
+
+
+
+ S3 secret access key for authentication.
+
+
+ )}
+ />
+ >
+ );
+};
diff --git a/app/server/modules/repositories/repository-config-secrets.ts b/app/server/modules/repositories/repository-config-secrets.ts
index d33c9647..2cfaec9c 100644
--- a/app/server/modules/repositories/repository-config-secrets.ts
+++ b/app/server/modules/repositories/repository-config-secrets.ts
@@ -18,6 +18,14 @@ export const mapRepositoryConfigSecrets = async (
accessKeyId: await transformSecret(config.accessKeyId),
secretAccessKey: await transformSecret(config.secretAccessKey),
};
+ case "b2":
+ return {
+ ...config,
+ customPassword,
+ cacert,
+ accountId: await transformSecret(config.accountId),
+ accountKey: await transformSecret(config.accountKey),
+ };
case "gcs":
return {
...config,
diff --git a/packages/core/src/restic/helpers/build-env.ts b/packages/core/src/restic/helpers/build-env.ts
index 4561f74a..a2d9666b 100644
--- a/packages/core/src/restic/helpers/build-env.ts
+++ b/packages/core/src/restic/helpers/build-env.ts
@@ -44,6 +44,10 @@ export const buildEnv = async (
env.AWS_REGION = "auto";
env.AWS_S3_FORCE_PATH_STYLE = "true";
break;
+ case "b2":
+ env.B2_ACCOUNT_ID = await deps.resolveSecret(config.accountId);
+ env.B2_ACCOUNT_KEY = await deps.resolveSecret(config.accountKey);
+ break;
case "gcs": {
const decryptedCredentials = await deps.resolveSecret(config.credentialsJson);
const credentialsPath = path.join("/tmp", `zerobyte-gcs-${crypto.randomBytes(8).toString("hex")}.json`);
diff --git a/packages/core/src/restic/helpers/build-repo-url.ts b/packages/core/src/restic/helpers/build-repo-url.ts
index ba896040..b985c436 100644
--- a/packages/core/src/restic/helpers/build-repo-url.ts
+++ b/packages/core/src/restic/helpers/build-repo-url.ts
@@ -15,6 +15,8 @@ export const buildRepoUrl = (config: RepositoryConfig): string => {
.replace(/\/$/, "");
return `s3:${endpoint}/${config.bucket}`;
}
+ case "b2":
+ return `b2:${config.bucket}:${config.path}`;
case "gcs":
return `gs:${config.bucket}:/`;
case "azure":
diff --git a/packages/core/src/restic/schemas.ts b/packages/core/src/restic/schemas.ts
index c38dfc1f..3c03456d 100644
--- a/packages/core/src/restic/schemas.ts
+++ b/packages/core/src/restic/schemas.ts
@@ -4,6 +4,7 @@ export const REPOSITORY_BACKENDS = {
local: "local",
s3: "s3",
r2: "r2",
+ b2: "b2",
gcs: "gcs",
azure: "azure",
rclone: "rclone",
@@ -50,6 +51,16 @@ export const s3RepositoryConfigSchema = z
})
.extend(baseRepositoryConfigSchema.shape);
+export const b2RepositoryConfigSchema = z
+ .object({
+ backend: z.literal("b2"),
+ bucket: z.string().min(1),
+ path: z.string().min(1),
+ accountId: z.string().min(1),
+ accountKey: z.string().min(1),
+ })
+ .extend(baseRepositoryConfigSchema.shape);
+
export const r2RepositoryConfigSchema = z
.object({
backend: z.literal("r2"),
@@ -124,6 +135,7 @@ export const sftpRepositoryConfigSchema = z
export const repositoryConfigSchemaBase = z.discriminatedUnion("backend", [
s3RepositoryConfigSchema,
r2RepositoryConfigSchema,
+ b2RepositoryConfigSchema,
localRepositoryConfigSchema,
gcsRepositoryConfigSchema,
azureRepositoryConfigSchema,