Skip to content

feat(api,web): lower free-tier storage to 250 MB + edit workspace limits in the admin panel#280

Merged
Zach Dunn (zachdunn) merged 8 commits into
mainfrom
claude/free-account-storage-limit-ba7f27
Jul 20, 2026
Merged

feat(api,web): lower free-tier storage to 250 MB + edit workspace limits in the admin panel#280
Zach Dunn (zachdunn) merged 8 commits into
mainfrom
claude/free-account-storage-limit-ba7f27

Conversation

@zachdunn

@zachdunn Zach Dunn (zachdunn) commented Jul 20, 2026

Copy link
Copy Markdown
Member

What & why

Two related changes to free-tier limits, prompted by the free-account storage cap being too generous for a screenshot-first host.

1. Lower the free-tier storage default: 1 GB → 250 MB (self-serve-defaults.ts)
New self-serve workspaces start at 250 MB instead of 1 GB. Uploads are WebP-converted by default, so 250 MB of stored assets holds far more screenshots than the raw byte figure suggests. Only affects newly created workspaces — existing ones keep their stamped limit. The monthly upload count (3000) and per-file caps (25 MB / 8 MB video) are unchanged.

2. Edit per-workspace budget limits from the admin panel
Previously the only way to change a workspace's limits was running scripts/set-workspace-limits.mjs from an operator's laptop (direct KV mutation via wrangler). This adds an in-app path for global admins:

  • validateLimitsPatch (workspace-limits.ts) — a pure, unit-tested validator: each of the four budget fields is a positive integer (set) or null (clear → unlimited); unknown keys ignored; bad values → 400 invalid_limit.
  • GET + PATCH /admin-ui/workspaces/:name/limits — session-cookie + requireAdminUser gated (same gate as the existing Workspaces panel). PATCH raw-reads the record, mutates only the four budget fields (set number / delete for unlimited), and writes the whole record back so key-policy, org linkage, and every other field survive. Unknown / soft-deleted / tombstoned workspaces → 404. Response includes current usage (null-tolerant).
  • Admin UI — a lazily-loaded Limits form in each expanded workspace row: number input + Unlimited checkbox per field, a unit dropdown (MB / GB / GiB) for byte fields, current-usage line, and a "changes apply within ~60s" note on save (the KV cacheTtl caveat).

Editable fields: maxStorageBytes, maxUploadsPerPeriod, maxUploadBytes, maxVideoUploadBytes. Retention/key-policy editing, a token-gated /admin twin, and audit logging are intentionally out of scope (the CLI script still covers those).

Testing

  • API suites green: workspace-limits (7), admin-ui (28 = 19 existing + 9 new), self-serve-defaults (3), routes-budget (6) — 44/44.
  • Web typecheck (astro check && tsc): 0 errors / 0 warnings.
  • New endpoint tests cover: set numbers, clear-to-unlimited, omitted-field preservation, non-budget-field preservation, 400 on invalid value, 404 unknown/soft-deleted, 403 non-admin.

Browser E2E deferred — needs the full 3-server local stack + a seeded admin session (same precedent as #268/#278). Logic is covered by the tests above plus a clean whole-branch review (auth gating, whole-record write-back, no unescaped innerHTML, contract match, unlimited round-trip, non-retroactive 250 MB change all verified).

Known minor follow-ups (non-blocking)

  • formatBytes(0) renders "0 GiB" rather than "0 MB" on a zero-usage workspace (cosmetic).
  • Defensive catch around the usage read is currently unreachable (getWorkspaceUsage returns an empty row rather than throwing).

Docs

Design + implementation plan under docs/superpowers/{specs,plans}/2026-07-20-admin-workspace-limits-editing*.

Summary by CodeRabbit

  • New Features

    • Added admin controls to view and edit workspace storage, upload, and video limits.
    • Added usage visibility, unlimited options, unit conversion, and inline save/error feedback.
    • Changes can be applied partially while preserving existing settings.
  • Bug Fixes

    • Added clear validation errors for invalid limit values and unknown or deleted workspaces.
  • Configuration

    • Reduced the default self-serve storage limit from 1 GB to 250 MB.

@zachdunn Zach Dunn (zachdunn) added the coderabbit:review Trigger CodeRabbit review for the PR. label Jul 20, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 20, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-api bfad9fe Commit Preview URL

Branch Preview URL
Jul 20 2026, 12:41 PM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-auth b2bedc5 Commit Preview URL

Branch Preview URL
Jul 20 2026, 12:26 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 20, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-web bfad9fe Commit Preview URL

Branch Preview URL
Jul 20 2026, 12:41 PM

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f071f6ce-8339-4e72-b814-187e46ee9821

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds admin API and UI support for viewing and editing four workspace budget limits, including unlimited values, usage display, validation, persistence, and tests. Also lowers the self-serve storage default from 1 GB to 250 MB.

Changes

Workspace limits editing

Layer / File(s) Summary
Limit validation contract
apps/api/src/workspace-limits.ts, packages/errors/src/codes.ts, apps/api/src/workspace-limits.test.ts
Defines the four editable fields, validates positive integer or null values, ignores unknown fields, and registers invalid_limit.
Admin limits API and persistence
apps/api/src/routes/admin-ui.ts, apps/api/src/routes/admin-ui.test.ts
Adds authenticated GET/PATCH endpoints with usage lookup, workspace checks, partial updates, unlimited clearing, full-record persistence, and route coverage.
Workspace limits editor
apps/web/src/pages/admin/index.astro, apps/web/src/styles/admin-workspaces.css
Adds lazy loading, editable inputs, unlimited toggles, byte-unit conversion, submission states, and styling.
Supporting design documentation
docs/superpowers/specs/..., docs/superpowers/plans/...
Documents the endpoint, validation, UI, persistence, testing, and propagation-delay behavior.

Self-serve storage default

Layer / File(s) Summary
Self-serve storage configuration
apps/api/src/self-serve-defaults.ts, apps/api/src/self-serve-defaults.test.ts
Changes the self-serve storage limit to 250_000_000 bytes and updates expectations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant AdminUI
  participant Registry
  participant Usage
  Admin->>AdminUI: Open workspace limits
  AdminUI->>Registry: Load workspace record
  AdminUI->>Usage: Fetch current usage
  Usage-->>AdminUI: Usage or null
  AdminUI-->>Admin: Render limits and usage
  Admin->>AdminUI: Save limit patch
  AdminUI->>Registry: Persist updated workspace record
  AdminUI-->>Admin: Return refreshed limits
Loading

Possibly related PRs

Poem

I nudge the limits, one field at a time,
With nulls for freedom and numbers that climb.
The admin can save, the usage can show,
And self-serve storage now has less to stow.
Hop, hop—250 MB in the burrow below!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the two main changes: lowering free-tier storage and adding admin editing for workspace limits.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/free-account-storage-limit-ba7f27

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/pages/admin/index.astro (1)

283-317: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Load limits independently of organization data.

Workspaces without an organization return at Lines 283-287 before fetching limits, and a members/invites failure also prevents the limits request. Those workspaces therefore cannot administer limits despite the API supporting them. Load limits before the organization early return and keep its retry state independent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/pages/admin/index.astro` around lines 283 - 317, Update the
workspace-loading flow around the organization guard and limits request so
limits are fetched and rendered for every workspace, including those without an
organization and when members/invites loading fails. Move the limits-loading
logic out of the organization-dependent try path, preserve the existing
renderLimitsForm and failure message, and keep its retry/loading state
independent from members and invites.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/routes/admin-ui.ts`:
- Around line 297-308: The PATCH /workspaces/:name/limits route must apply the
required workspace-keyed writeRateLimit middleware before performing the KV
mutation. Update the route definition around the limits patch handler to include
writeRateLimit using the workspace name key, while preserving the existing
validation, record update, and response behavior.
- Around line 299-307: Update the workspace mutation flow containing
loadEditableWorkspace and REGISTRY.put so concurrent PATCH requests cannot
overwrite each other’s limit changes. Serialize full-record mutations for each
workspace, or use the registry’s transactional/versioned update mechanism, while
preserving the existing patch validation and field deletion/update behavior.
- Line 300: Update the request-body parsing before validateLimitsPatch in the
admin route to distinguish malformed JSON from an intentionally empty object.
Catch parse failures and throw a structured ValidationError before validation or
persistence, while preserving normal handling for successfully parsed bodies.

---

Outside diff comments:
In `@apps/web/src/pages/admin/index.astro`:
- Around line 283-317: Update the workspace-loading flow around the organization
guard and limits request so limits are fetched and rendered for every workspace,
including those without an organization and when members/invites loading fails.
Move the limits-loading logic out of the organization-dependent try path,
preserve the existing renderLimitsForm and failure message, and keep its
retry/loading state independent from members and invites.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 38e34c75-cbd3-43ac-9512-56849951cccd

📥 Commits

Reviewing files that changed from the base of the PR and between 639d76b and b2bedc5.

📒 Files selected for processing (11)
  • apps/api/src/routes/admin-ui.test.ts
  • apps/api/src/routes/admin-ui.ts
  • apps/api/src/self-serve-defaults.test.ts
  • apps/api/src/self-serve-defaults.ts
  • apps/api/src/workspace-limits.test.ts
  • apps/api/src/workspace-limits.ts
  • apps/web/src/pages/admin/index.astro
  • apps/web/src/styles/admin-workspaces.css
  • docs/superpowers/plans/2026-07-20-admin-workspace-limits-editing.md
  • docs/superpowers/specs/2026-07-20-admin-workspace-limits-editing-design.md
  • packages/errors/src/codes.ts

Comment on lines +297 to +308
.patch("/workspaces/:name/limits", async (c) => {
const name = c.req.param("name");
const record = await loadEditableWorkspace(c.env, name);
const patch = validateLimitsPatch(await c.req.json().catch(() => ({})));
for (const field of LIMIT_FIELDS) {
if (!(field in patch)) continue;
const value = patch[field];
if (value === null) delete record[field];
else record[field] = value;
}
await c.env.REGISTRY.put(`ws:${name}`, JSON.stringify(record));
return c.json(await limitsResponse(c.env, name, record));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Rate-limit this workspace mutation.

This route writes KV without the required workspace-keyed writeRateLimit middleware, allowing an authenticated admin to issue unbounded writes.

As per coding guidelines, “Mutating routes must use the writeRateLimit middleware keyed by workspace; the middleware may no-op only when the WRITE_LIMITER binding is absent.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/admin-ui.ts` around lines 297 - 308, The PATCH
/workspaces/:name/limits route must apply the required workspace-keyed
writeRateLimit middleware before performing the KV mutation. Update the route
definition around the limits patch handler to include writeRateLimit using the
workspace name key, while preserving the existing validation, record update, and
response behavior.

Source: Coding guidelines

Comment on lines +299 to +307
const record = await loadEditableWorkspace(c.env, name);
const patch = validateLimitsPatch(await c.req.json().catch(() => ({})));
for (const field of LIMIT_FIELDS) {
if (!(field in patch)) continue;
const value = patch[field];
if (value === null) delete record[field];
else record[field] = value;
}
await c.env.REGISTRY.put(`ws:${name}`, JSON.stringify(record));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Prevent concurrent full-record writes from losing updates.

Two PATCH requests can both read the old record and each write a full replacement; the later write silently removes the other request’s limit changes. Serialize workspace-record mutations or use a transactional/versioned persistence mechanism.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/admin-ui.ts` around lines 299 - 307, Update the workspace
mutation flow containing loadEditableWorkspace and REGISTRY.put so concurrent
PATCH requests cannot overwrite each other’s limit changes. Serialize
full-record mutations for each workspace, or use the registry’s
transactional/versioned update mechanism, while preserving the existing patch
validation and field deletion/update behavior.

Comment thread apps/api/src/routes/admin-ui.ts Outdated
.patch("/workspaces/:name/limits", async (c) => {
const name = c.req.param("name");
const record = await loadEditableWorkspace(c.env, name);
const patch = validateLimitsPatch(await c.req.json().catch(() => ({})));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject malformed JSON instead of persisting a no-op.

Line 300 converts a JSON parse failure to {}, so invalid request bodies return 200 and still write the record. Throw a structured ValidationError before calling validateLimitsPatch.

Proposed fix
-    const patch = validateLimitsPatch(await c.req.json().catch(() => ({})));
+    const body = await c.req.json().catch(() => {
+      throw new ValidationError("limits body must be a JSON object", {
+        code: "invalid_limit",
+      });
+    });
+    const patch = validateLimitsPatch(body);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const patch = validateLimitsPatch(await c.req.json().catch(() => ({})));
const body = await c.req.json().catch(() => {
throw new ValidationError("limits body must be a JSON object", {
code: "invalid_limit",
});
});
const patch = validateLimitsPatch(body);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/admin-ui.ts` at line 300, Update the request-body parsing
before validateLimitsPatch in the admin route to distinguish malformed JSON from
an intentionally empty object. Catch parse failures and throw a structured
ValidationError before validation or persistence, while preserving normal
handling for successfully parsed bodies.

@zachdunn
Zach Dunn (zachdunn) merged commit d4f2319 into main Jul 20, 2026
5 checks passed
@zachdunn
Zach Dunn (zachdunn) deleted the claude/free-account-storage-limit-ba7f27 branch July 20, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coderabbit:review Trigger CodeRabbit review for the PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant