backupdrill 2.7.1 → 2.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -136,7 +136,8 @@ A backup pulls data **out** of Supabase, which counts as **egress on your Supaba
136
136
 
137
137
  - Backup traffic (pg_dump + first-time file downloads) is **uncached egress: $0.09/GB** above your plan's included allowance.
138
138
  - Included uncached egress: **Free 5 GB · Pro 250 GB · Team 250 GB** per month.
139
- - **Spend Cap is ON by default (Pro/Team).** If a backup pushes you past the quota, Supabase **throttles your whole project's egress** for the rest of the month and the backup fails — turn Spend Cap OFF to be billed the overage instead.
139
+ - **Spend Cap exists only on Pro, and it's ON by default.** With it on, going past the quota isn't billed. Supabase emails your billing address and starts a grace period, and if usage stays over it **restricts your organization's projects** (your app, not just the backup) until the next billing cycle or until you turn Spend Cap OFF. The grace period isn't renewed every time: go over again before the dashboard's notice clears (that takes several billing cycles under the limits) and the restriction can start right away. Turn Spend Cap OFF up front to be billed the overage instead.
140
+ - **Team has no Spend Cap**, so egress past the included 250 GB is always billed at $0.09/GB.
140
141
  - **Free never bills overage.** Exceeding the included 5 GB/month starts Supabase's Fair Use process: a notification to your billing email, then a grace period, then egress restrictions if usage stays over (the restriction clears at the next billing cycle or when you upgrade). Sustained large backups are not viable on Free.
141
142
 
142
143
  Rough cost (Pro, Spend Cap OFF, backups = your only egress):
@@ -163,6 +164,8 @@ backupdrill estimate --plan pro # measures your DB + Storage, prints the proje
163
164
  <prefix>/<project>.drills/<finished-at>-<passed|failed>-<tag>.json # only with drill --record: one file per drill
164
165
  ```
165
166
 
167
+ Versions up to 2.7.1 took an empty `BACKUPDRILL_PROJECT_NAME` (what an unset GitHub Actions secret becomes) as the project name, so those backups sit under `<prefix>//<timestamp>/`. Later versions treat it as unset and take the name from your config file, or use `supabase` if the file doesn't set one. To drill or restore one of the older snapshots, pass `--project-name ''`, as its RECOVERY.md already does.
168
+
166
169
  ## Restoring & drilling
167
170
 
168
171
  Prove a backup is restorable, without touching anything real — `drill` restores the latest snapshot into a throwaway Postgres and checks it. It needs **Docker** (for the throwaway Postgres) and **`pg_restore`** (same major-version rule as `pg_dump`; set `BACKUPDRILL_PG_RESTORE` if it's not on your `PATH`):
package/dist/config.js CHANGED
@@ -6,6 +6,13 @@ export function requireBucket(config) {
6
6
  }
7
7
  return config.storage;
8
8
  }
9
+ /** "a, b" → ["a", "b"]。只有逗号和空白时算没设:空列表会盖掉配置文件里的值。 */
10
+ function listFromEnv(value) {
11
+ const items = value?.split(",").map((s) => s.trim()).filter(Boolean);
12
+ return items?.length ? items : undefined;
13
+ }
14
+ // 环境变量的空串一律算没设:GitHub Actions 里没配的 secret 展开就是空串,
15
+ // examples/scheduled-backup.yml 把每个可选项都这样传进来。
9
16
  function fromEnv() {
10
17
  const env = process.env;
11
18
  const storage = {};
@@ -33,16 +40,13 @@ function fromEnv() {
33
40
  if (env.BACKUPDRILL_SUPABASE_STORAGE_SECRET_ACCESS_KEY)
34
41
  supabaseStorage.secretAccessKey =
35
42
  env.BACKUPDRILL_SUPABASE_STORAGE_SECRET_ACCESS_KEY;
36
- if (env.BACKUPDRILL_SUPABASE_STORAGE_BUCKETS)
37
- supabaseStorage.buckets = env.BACKUPDRILL_SUPABASE_STORAGE_BUCKETS.split(",")
38
- .map((s) => s.trim())
39
- .filter(Boolean);
43
+ const buckets = listFromEnv(env.BACKUPDRILL_SUPABASE_STORAGE_BUCKETS);
44
+ if (buckets)
45
+ supabaseStorage.buckets = buckets;
40
46
  return {
41
- databaseUrl: env.BACKUPDRILL_DATABASE_URL || env.DATABASE_URL,
42
- projectName: env.BACKUPDRILL_PROJECT_NAME,
43
- schemas: env.BACKUPDRILL_SCHEMAS
44
- ? env.BACKUPDRILL_SCHEMAS.split(",").map((s) => s.trim()).filter(Boolean)
45
- : undefined,
47
+ databaseUrl: env.BACKUPDRILL_DATABASE_URL || env.DATABASE_URL || undefined,
48
+ projectName: env.BACKUPDRILL_PROJECT_NAME || undefined,
49
+ schemas: listFromEnv(env.BACKUPDRILL_SCHEMAS),
46
50
  storage,
47
51
  supabaseStorage,
48
52
  };
@@ -91,6 +95,8 @@ export async function loadConfig(opts) {
91
95
  const env = fromEnv();
92
96
  const o = opts.overrides;
93
97
  const databaseUrl = o.databaseUrl ?? env.databaseUrl ?? file.databaseUrl;
98
+ // 显式传 --project-name '' 仍按空名处理:2.7.1 及以前,环境变量的空串会让快照落在
99
+ // <prefix>//<时间戳>/,那些快照里的 RECOVERY.md 写的就是 --project-name '',得能找回来
94
100
  const projectName = o.projectName ?? env.projectName ?? file.projectName ?? "supabase";
95
101
  const schemas = o.schemas ?? env.schemas ?? file.schemas ?? ["public"];
96
102
  const storage = {
@@ -11,6 +11,7 @@ export interface EgressPricing {
11
11
  includedGb: number;
12
12
  planLabel: string;
13
13
  hardCap: boolean;
14
+ spendCap?: boolean;
14
15
  }
15
16
  /**
16
17
  * 要转储的 schema 里所有表的磁盘占用。pg_total_relation_size 含索引+TOAST,是 egress
@@ -32,4 +33,6 @@ export interface EgressProjection {
32
33
  }>;
33
34
  }
34
35
  export declare function project(dbBytes: number, storageBytes: number, pricing: EgressPricing): EgressProjection;
36
+ /** 付费计划超出 egress 额度时会怎样:Pro 看 Spend Cap(默认开,不计费但可能被限),Team 没有 Spend Cap、直接计费 */
37
+ export declare function overageNote(pricing: EgressPricing): string;
35
38
  export declare function runEstimate(config: BackupConfig, pricing: EgressPricing): Promise<EgressProjection>;
package/dist/estimate.js CHANGED
@@ -45,6 +45,21 @@ export function project(dbBytes, storageBytes, pricing) {
45
45
  });
46
46
  return { dbBytes, storageBytes, perRunGb, rows };
47
47
  }
48
+ /** 付费计划超出 egress 额度时会怎样:Pro 看 Spend Cap(默认开,不计费但可能被限),Team 没有 Spend Cap、直接计费 */
49
+ export function overageNote(pricing) {
50
+ if (pricing.spendCap === false) {
51
+ return (`${pricing.planLabel} has no Spend Cap: egress past the ${pricing.includedGb} GB ` +
52
+ `quota is billed at $${pricing.pricePerGb}/GB, so the numbers above are what you'd pay.`);
53
+ }
54
+ // 开着 Spend Cap 超额不是立刻断:Supabase 先发邮件、给宽限期,持续超额才限制整个组织的项目;
55
+ // 宽限期用过一次后,几个账期内再超就没有新的宽限期(文档 Exceeding Quotas + Fair Use Policy,2026-10-29 核)
56
+ return ("Spend Cap is ON by default on Pro. Going past the " +
57
+ `${pricing.includedGb} GB egress quota with it on isn't billed: Supabase emails ` +
58
+ "your billing address, starts a grace period (none if you went over recently), " +
59
+ "then restricts your organization's " +
60
+ "projects if usage stays over (your app, not just the backup). Turn Spend Cap OFF " +
61
+ "to be billed for the overage instead, and the numbers above are what you'd pay.");
62
+ }
48
63
  export async function runEstimate(config, pricing) {
49
64
  log.step("Measuring database size…");
50
65
  const dbBytes = await measureDbBytes(config.databaseUrl, config.schemas);
@@ -91,12 +106,7 @@ export async function runEstimate(config, pricing) {
91
106
  }
92
107
  }
93
108
  else {
94
- // Pro/Team:Spend Cap 决定是"限流"还是"账单"
95
- log.warn("Spend Cap is ON by default on Pro/Team. If a backup pushes you past the " +
96
- `${pricing.includedGb} GB egress quota, Supabase throttles your project's ` +
97
- "egress for the rest of the month (your app's traffic too), and the backup " +
98
- "fails. Turn Spend Cap OFF to allow billed overage instead — then the " +
99
- "numbers above are what you'd pay.");
109
+ log.warn(overageNote(pricing));
100
110
  const worst = daily?.monthlyCost ?? 0;
101
111
  if (worst >= SUBSCRIPTION_FLOOR_USD) {
102
112
  log.warn(`At daily frequency the egress overage alone (~$${worst.toFixed(0)}/month) is ` +
package/dist/index.js CHANGED
@@ -35,11 +35,11 @@ import { TOOL_VERSION } from "./version.js";
35
35
  // Supabase egress 定价表(2026-07-10 核实,来源见 README「Egress & cost」)。
36
36
  // 备份流量 100% 未缓存,故用 uncached 单价 $0.09/GB;included 用 uncached 额度。
37
37
  // Free 永不计费:超额走 Fair Use 流程(通知 → 宽限期 → 限制),持续大备份不可行;
38
- // Pro/Team 由 Spend Cap 决定超额限流还是计费。
38
+ // 只有 Pro 有 Spend Cap(默认开,超额不计费、持续超额被限,关掉才计费);Team 没有 Spend Cap,超额直接计费。
39
39
  const EGRESS_PRICING = {
40
40
  free: { planLabel: "Free", includedGb: 5, pricePerGb: 0.09, hardCap: true },
41
- pro: { planLabel: "Pro", includedGb: 250, pricePerGb: 0.09, hardCap: false },
42
- team: { planLabel: "Team", includedGb: 250, pricePerGb: 0.09, hardCap: false },
41
+ pro: { planLabel: "Pro", includedGb: 250, pricePerGb: 0.09, hardCap: false, spendCap: true },
42
+ team: { planLabel: "Team", includedGb: 250, pricePerGb: 0.09, hardCap: false, spendCap: false },
43
43
  };
44
44
  // backup 和 drill 共用:按 Healthchecks.io 的约定报告开始、成功、失败
45
45
  const PING_FLAG = "--ping-url <url>";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backupdrill",
3
- "version": "2.7.1",
3
+ "version": "2.7.2",
4
4
  "description": "Open-source CLI to back up a Supabase project — Postgres database and Storage files — to your own S3/R2/B2 bucket, with a checksummed manifest.",
5
5
  "type": "module",
6
6
  "bin": {
package/plugin/README.md CHANGED
@@ -7,7 +7,7 @@ Back up a Supabase project to a bucket you own and prove the backup restores, by
7
7
  - `drill` restores a snapshot into a throwaway Postgres in Docker, checks that the tables and their data came back, and checksums a sample of the Storage files.
8
8
  - `restore` puts a snapshot into a new, empty Supabase project, after a dry run and only once you confirm the target.
9
9
 
10
- The agent runs every command as `npx -y backupdrill@2.7.0`, so the CLI version stays fixed.
10
+ The agent runs every command as `npx -y backupdrill@2.7.1`, so the CLI version stays fixed.
11
11
 
12
12
  ## What you need
13
13
 
@@ -7,7 +7,7 @@ description: Use when the user wants to back up a Supabase project to their own
7
7
 
8
8
  backupdrill is an open-source (MIT) command-line tool. `backup` dumps the project's Postgres database with `pg_dump`, copies Storage files too once their S3 keys are set, and writes everything to a bucket the user owns, with a checksummed `manifest.json` and a `RECOVERY.md` in every snapshot. `drill` restores a snapshot into a throwaway Postgres in Docker and checks that it came back. The CLI runs on the user's machine. It talks to their Supabase project, their bucket and the local Docker daemon, and sends nothing to BackupDrill.
9
9
 
10
- Always run it as `npx -y backupdrill@2.7.0 <command>`, so every run uses the same version.
10
+ Always run it as `npx -y backupdrill@2.7.1 <command>`, so every run uses the same version.
11
11
 
12
12
  You need a shell on the user's machine for this. If you don't have one, walk the user through the same commands instead of running them.
13
13
 
@@ -17,7 +17,7 @@ The CLI needs a database connection string and bucket keys. They must never appe
17
17
 
18
18
  1. Keep them in `.env.backupdrill` in the project root. Before creating it, make sure Git ignores it and isn't already tracking it: `git check-ignore -q .env.backupdrill || printf '\n.env.backupdrill\n' >> .gitignore; git ls-files --error-unmatch .env.backupdrill >/dev/null 2>&1 && echo 'already tracked by Git'`. An ignore rule doesn't stop Git from committing a file it already tracks. If that prints `already tracked by Git`, stop: ask the user to run `git rm --cached .env.backupdrill`, commit that, and rotate any key that was ever committed in the file.
19
19
  2. If the file doesn't exist yet, create it private from the start with `(umask 077 && touch .env.backupdrill); chmod -N .env.backupdrill 2>/dev/null; true`, then write the placeholders below into it and ask the user to fill it in their editor. If it exists, leave its contents alone, but make sure only the user can read it: `chmod 600 .env.backupdrill; chmod -N .env.backupdrill 2>/dev/null; true`. On macOS `chmod -N` drops ACL entries, which `chmod 600` leaves in place and which can still let other users read the file; elsewhere it fails quietly. Don't ask for keys in the chat. If the user pastes one anyway, don't repeat it, and suggest rotating it.
20
- 3. Never open, cat or print the file, and never echo a variable loaded from it. The commands in this skill are the only way to touch it. To add a non-secret line, append it with `printf '\n%s\n' "NAME='value'" >> .env.backupdrill` (the leading newline keeps it off the user's last line).
20
+ 3. Never open or print the file, and never echo a variable loaded from it. The commands in this skill are the only way to touch it. To add a non-secret line, append it with `printf '\n%s\n' "NAME='value'" >> .env.backupdrill` (the leading newline keeps it off the user's last line).
21
21
  4. Run every command exactly as written below. Each one starts a subshell that turns off shell tracing, drops any `BACKUPDRILL_*` or `DATABASE_URL` values inherited from the user's shell, so a broken line can't fall back to some other database, then loads the file and stops if it can't be read. The `2>/dev/null` keeps a malformed line from printing its value.
22
22
  5. The CLI also reads `backupdrill.config.json` from the project root if there is one, and `.env.backupdrill` wins value by value. If the check below reports that file, tell the user and settle on one place: move its settings into `.env.backupdrill` and delete it, or keep it on purpose.
23
23
 
@@ -69,7 +69,7 @@ Every backup reads the whole database, and every configured Storage file, out of
69
69
  ```sh
70
70
  ( set +x; unset DATABASE_URL $(env | sed -n 's/^\(BACKUPDRILL_[A-Za-z0-9_]*\)=.*/\1/p')
71
71
  set -a; . ./.env.backupdrill 2>/dev/null || { echo ".env.backupdrill could not be loaded" >&2; exit 1; }; set +a
72
- npx -y backupdrill@2.7.0 estimate --plan pro )
72
+ npx -y backupdrill@2.7.1 estimate --plan pro )
73
73
  ```
74
74
 
75
75
  Use `--plan free`, `pro` or `team` to match their Supabase plan, and read the projection back before suggesting a daily schedule. On a large database, suggest weekly.
@@ -80,7 +80,7 @@ Use `--plan free`, `pro` or `team` to match their Supabase plan, and read the pr
80
80
  report="$(mktemp "${TMPDIR:-/tmp}/backupdrill-backup.XXXXXX")" || exit 1; echo "stdout → $report"
81
81
  ( set +x; unset DATABASE_URL $(env | sed -n 's/^\(BACKUPDRILL_[A-Za-z0-9_]*\)=.*/\1/p')
82
82
  set -a; . ./.env.backupdrill 2>/dev/null || { echo ".env.backupdrill could not be loaded" >&2; exit 1; }; set +a
83
- npx -y backupdrill@2.7.0 backup ) > "$report"
83
+ npx -y backupdrill@2.7.1 backup ) > "$report"
84
84
  ```
85
85
 
86
86
  - The progress lines on stderr show where everything went (`Dumping → s3://…`, `Manifest written → s3://…`) and end with `Backup complete — N tables + M Storage files`. stdout goes to the file named on the first line, a new one each run that only the user can read: one line of JSON with the whole manifest, which lists every table and Storage file, so read only the fields you need from it.
@@ -93,7 +93,7 @@ report="$(mktemp "${TMPDIR:-/tmp}/backupdrill-backup.XXXXXX")" || exit 1; echo "
93
93
  ```sh
94
94
  ( set +x; unset DATABASE_URL $(env | sed -n 's/^\(BACKUPDRILL_[A-Za-z0-9_]*\)=.*/\1/p')
95
95
  set -a; . ./.env.backupdrill 2>/dev/null || { echo ".env.backupdrill could not be loaded" >&2; exit 1; }; set +a
96
- npx -y backupdrill@2.7.0 drill )
96
+ npx -y backupdrill@2.7.1 drill )
97
97
  ```
98
98
 
99
99
  - It reads the latest snapshot from the bucket, restores it into a throwaway Postgres container, runs its checks and removes the container. It doesn't connect to the Supabase project. The first run pulls a Postgres image from Docker Hub.
@@ -112,7 +112,7 @@ A restore writes data, so go step by step and let the user drive.
112
112
  ```sh
113
113
  ( set +x; unset DATABASE_URL $(env | sed -n 's/^\(BACKUPDRILL_[A-Za-z0-9_]*\)=.*/\1/p')
114
114
  set -a; . ./.env.backupdrill 2>/dev/null || { echo ".env.backupdrill could not be loaded" >&2; exit 1; }; set +a
115
- npx -y backupdrill@2.7.0 restore --database --target-supabase-url https://<target-ref>.supabase.co --dry-run )
115
+ npx -y backupdrill@2.7.1 restore --database --target-supabase-url https://<target-ref>.supabase.co --dry-run )
116
116
  ```
117
117
 
118
118
  Leave out `--target-supabase-url` when the snapshot is database only. To write Storage files to a local folder instead of the new project, use `--storage-dir ./recovered`.
@@ -95,7 +95,7 @@ jobs:
95
95
 
96
96
  - name: Run backupdrill
97
97
  # Pinned so every run uses the same CLI. Raise it on purpose when you upgrade.
98
- run: npx -y backupdrill@2.7.0 backup
98
+ run: npx -y backupdrill@2.7.1 backup
99
99
  env:
100
100
  # --- required ---
101
101
  BACKUPDRILL_DATABASE_URL: ${{ secrets.BACKUPDRILL_DATABASE_URL }}