Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

feat(backend): add insforge.toml config-as-code routes#1200

Open
tonychang04 wants to merge 8 commits intomainInsForge/InsForge:mainfrom
feat/insforge-config-tomlInsForge/InsForge:feat/insforge-config-tomlCopy head branch name to clipboard
Open

feat(backend): add insforge.toml config-as-code routes#1200
tonychang04 wants to merge 8 commits intomainInsForge/InsForge:mainfrom
feat/insforge-config-tomlInsForge/InsForge:feat/insforge-config-tomlCopy head branch name to clipboard

Conversation

@tonychang04
Copy link
Copy Markdown
Contributor

@tonychang04 tonychang04 commented May 6, 2026

Companion to: InsForge/CLI#100 (CLI half — talks to the routes this PR adds)

What

Adds the server-side half of insforge.toml config-as-code. Two new endpoints under /api/config, idempotent apply orchestration with a Postgres advisory lock, and an agent-facing doc.

GET  /api/config              → { config: <JSON> }   # current live state
POST /api/config/apply        → body: { config, dry_run, prune }
                                returns: { plan, applied }

Server-side apply validates the entire desired state before any mutation, applies each section idempotently (auth via setAuthSettings, buckets via upsertBucket / deleteBucket), and is wrapped in a pg_advisory_lock keyed on (LOCK_NAMESPACE, hash(projectRef)) so concurrent applies serialize per project.

Why

See InsForge/CLI#100 for the full motivation. Short version: agents and humans review code through diffs, not API call logs. A TOML intent layer makes config changes reviewable, repo-clonable, and git revert-able. DB stays source of truth; the TOML is intent.

What's in this PR (backend half)

8 commits across 11 files:

New service module at backend/src/services/config/:

  • schema.ts — config JSON validator (mirrors CLI; intentional duplication for v1, hoist to @insforge/shared-schemas later)
  • read.ts — projects live state from existing services into the JSON shape
  • diff.ts + tests — server-side diff (mirror of CLI for sanity)
  • lock.ts + tests — Postgres advisory lock helper
  • apply.ts + tests — idempotent orchestration

New routes at backend/src/api/routes/config/:

  • get.ts, apply.ts, index.routes.ts — mounted at /api/config in server.ts

Service-layer additions (small, named):

  • services/auth/settings.ts — clean wrapper over AuthConfigService for stable seams
  • services/storage/buckets.ts — adds upsertBucket (didn't exist; needed for idempotence)

Agent doc: docs/agent-docs/config-as-code.md

Key behaviors

  • Idempotent. All ops are upsert; converged-state apply = no-op.
  • Default-keep. Items in DB missing from file → kept, warned. --prune opts in.
  • Server-side validation before mutation. ConfigValidationError → 400 with structured message.
  • Advisory lock per project per apply. Two concurrent applies serialize; different projects don't block.
  • Sensitive fields via env(NAME). Resolved server-side at apply time.

MVP scope (deliberately narrow)

Wired in v1: [auth] additional_redirect_urls and [storage.buckets.<name>] public. Discovered during recon that jwt_expiry/enable_signup/site_url don't have backing columns in auth.config; shipping them would silently no-op and break idempotence (read returns undefined, file says X, plan shows infinite drift). Honest fix: shrink MVP, punt those three to a follow-up plan that adds columns first.

OAuth providers, function metadata, SMTP, schedules, AI provider config, etc. — all are matrix-fill follow-ups. Architecture is identical for each; just plumbing.

Tests

src/services/config/diff.test.ts     5 tests passed
src/services/config/lock.test.ts     4 tests passed
src/services/config/apply.test.ts    5 tests passed
Total:                              14 tests passed (3 files)

lock.test.ts uses an in-memory fake Pool matching the existing repo convention (backend/tests/unit/payments-advisory-lock.test.ts). Real-DB integration test will come with a follow-up that adds shared pg test infra.

Test plan

  • npx vitest run src/services/config/diff.test.ts src/services/config/lock.test.ts src/services/config/apply.test.ts — 14/14 green
  • Targeted typecheck of new files clean
  • Manual smoke against running docker compose instance (requires CLI half deployed)
  • Verify GET endpoint with curl + Bearer token

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added configuration management API with GET and POST endpoints for retrieving and applying project settings.
    • Introduced dry-run mode to preview configuration changes before committing them.
    • Configuration diffing enables detection of changes to authentication redirect URLs and storage buckets.
    • Added support for idempotent configuration updates with optional orphaned bucket cleanup.
  • Documentation

    • Added config-as-code guide documenting the declarative configuration workflow.

tonychang04 and others added 8 commits May 5, 2026 17:22
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds settings.ts and buckets.ts thin wrappers around the existing
AuthConfigService and StorageService so the config service has clean
seams. upsertBucket lives in storage/buckets.ts and provides the
idempotent create-or-update semantic the spec requires.

Note: jwt_expiry, enable_signup, and site_url have no backing columns
in auth.config today; the reader returns undefined for them and the
writer is a no-op for those fields until the schema grows real
columns. allowedRedirectUrls maps to additional_redirect_urls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the new config service into the API layer. The OSS backend is
single-tenant — one Postgres per InsForge instance — so all
config-apply requests serialize on a fixed advisory key (`default`).
Multi-tenant cloud deployments would supply req.projectId from auth
middleware.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s only

Of the four originally-scoped [auth] fields, only additional_redirect_urls
is backed by a real column on auth.config today. Sending jwt_expiry,
enable_signup, or site_url through setAuthSettings silently no-ops, which
breaks idempotence: the diff would re-surface them on every plan.

Trim schema, diff, apply, read, and the settings wrapper down to just
additional_redirect_urls. The other three fields are punted to a follow-up
plan that adds the backing columns to auth.config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Walkthrough

Introduces a complete configuration-as-code feature for backend infrastructure. Adds schema validation, config diffing, config application with dry-run and prune modes, PostgreSQL advisory locking for concurrency, authentication and storage bucket management wrappers, HTTP API endpoints, and supporting documentation. Routes are integrated into the main server.

Changes

Configuration Management System

Layer / File(s) Summary
Schema & Types
backend/src/services/config/schema.ts, backend/src/services/auth/settings.ts, backend/src/services/storage/buckets.ts
Config type hierarchy (InsforgeConfig, AuthConfig, StorageConfig, BucketConfig) with validation error class. Auth and storage bucket interfaces and wrappers for reading/writing settings and bucket state.
Config Reading
backend/src/services/config/read.ts
Concurrently fetches auth settings and storage buckets to construct live InsforgeConfig representation.
Config Diffing
backend/src/services/config/diff.ts, backend/src/services/config/diff.test.ts
Computes changes between live and target configs with discriminated union DiffChange type for auth and bucket modifications. Supports optional pruning of removed buckets. Includes detailed test coverage for add/modify/remove scenarios and idempotence.
Config Application Orchestration
backend/src/services/config/apply.ts, backend/src/services/config/apply.test.ts
Validates input, reads live state, computes plan, and applies changes in two phases: auth redirect URLs and storage buckets (upsert/delete). Supports dry-run and prune modes. Returns plan and applied flag. Test suite covers plan-only runs, applying modifications, pruning behavior, and convergence scenarios.
Concurrency Locking
backend/src/services/config/lock.ts, backend/src/services/config/lock.test.ts
PostgreSQL advisory-lock wrapper per project. Hashes project ref to 32-bit key using SHA-256, acquires lock before applying config, and releases on completion or error. Test suite uses fake pool to verify serialization across projects and non-blocking behavior for different projects.
HTTP API Routes
backend/src/api/routes/config/get.ts, backend/src/api/routes/config/apply.ts, backend/src/api/routes/config/index.routes.ts
GET /api/config with verifyAdmin returns live config via readLiveConfig. POST /api/config/apply with verifyAdmin validates input, acquires per-project lock, invokes applyConfig, and returns result or 400 on validation error. Router module wires both endpoints.
Server Integration
backend/src/server.ts
Imports and mounts configRouter at /api/config within main API router.
Documentation
docs/agent-docs/config-as-code.md
Guides users through export/plan/apply workflow, TOML structure, drift handling, idempotence, concurrency semantics, and wire-format expectations. References design and implementation plan docs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A config to rule them all, so neat,
Diffs and plans make validation sweet,
Apply with locks—no race condition here,
Auth and buckets, crystal clear!
From TOML to truth, the pipeline's bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding two new HTTP routes for insforge.toml config-as-code. It directly matches the primary objective of introducing GET and POST endpoints for configuration management.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/insforge-config-toml

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (4)
backend/src/services/storage/buckets.ts (1)

26-34: 💤 Low value

TOCTOU race in upsertBucket mitigated by advisory lock.

The check-then-act pattern (bucketExistscreateBucket) is susceptible to race conditions if called concurrently. Since the apply flow wraps calls in withConfigApplyLock, this is safe in practice. Consider adding a brief comment noting that callers must hold the config apply lock, or make the function internally safe with a try-catch around createBucket that handles "already exists" errors.

🤖 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 `@backend/src/services/storage/buckets.ts` around lines 26 - 34, The
upsertBucket function uses a check-then-act pattern (bucketExists →
createBucket) that is vulnerable to a TOCTOU race; make it explicit and safe by
either adding a short comment above upsertBucket that callers must hold the
withConfigApplyLock when calling it, or make the function itself resilient by
wrapping the createBucket call in a try-catch that treats an "already exists"
error as success and proceeds to update visibility (use the existing
bucketExists, createBucket, updateBucketVisibility symbols for context); prefer
the try-catch approach if you want internal safety, otherwise document the lock
requirement clearly.
backend/src/services/config/diff.ts (1)

106-117: 💤 Low value

deepEqual is limited to arrays and primitives.

The current implementation handles arrays and primitives but returns false for object comparisons (other than arrays). This is sufficient for the current MVP scope (comparing string arrays for redirect URLs), but if you later add nested object fields, this will need to be extended.

🤖 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 `@backend/src/services/config/diff.ts` around lines 106 - 117, deepEqual
currently only compares primitives and arrays and returns false for plain
objects; update the deepEqual function to also handle plain object comparison
by: treating null separately, detecting non-array objects (typeof === "object"
&& !Array.isArray), comparing their key sets length and presence, and
recursively calling deepEqual on each corresponding property value (use the
existing array handling for arrays and === for primitives as fallbacks). Ensure
the function still short-circuits identical references (a === b) and preserves
array handling in deepEqual.
backend/src/services/config/schema.ts (1)

32-51: ⚖️ Poor tradeoff

Consider using Zod schemas for validation.

The manual validation logic works but is more verbose and doesn't provide automatic type inference. As per coding guidelines: "Use Zod schemas for request/response validation in backend". Zod would also give you stricter validation (rejecting unknown keys) and better error messages out of the box.

Example Zod schema structure
import { z } from 'zod';

export const BucketConfigSchema = z.object({
  public: z.boolean().optional(),
}).strict();

export const StorageConfigSchema = z.object({
  buckets: z.record(z.string(), BucketConfigSchema).optional(),
}).strict();

export const AuthConfigSchema = z.object({
  additional_redirect_urls: z.array(z.string()).optional(),
}).strict();

export const InsforgeConfigSchema = z.object({
  project_id: z.string().optional(),
  auth: AuthConfigSchema.optional(),
  storage: StorageConfigSchema.optional(),
}).strict();

export type InsforgeConfig = z.infer<typeof InsforgeConfigSchema>;

export function validateConfig(input: unknown): InsforgeConfig {
  return InsforgeConfigSchema.parse(input);
}
🤖 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 `@backend/src/services/config/schema.ts` around lines 32 - 51, Replace the
hand-rolled validation in validateConfig with a Zod schema: add imports from z,
define BucketConfigSchema, StorageConfigSchema, AuthConfigSchema and
InsforgeConfigSchema using z.object(...).strict() and optional() for optional
fields, derive the InsforgeConfig type with z.infer<typeof
InsforgeConfigSchema>, and update validateConfig to simply return
InsforgeConfigSchema.parse(input); remove the manual checks around
project_id/auth/storage and stop calling validateAuth/validateStorage (instead
embed their schemas or reuse them) so unknown keys are rejected and Zod error
messages are returned.
backend/src/api/routes/config/apply.ts (1)

14-16: ⚡ Quick win

Prefer env-backed project fallback before 'default'.

Line 15 currently falls back directly to 'default'. To match existing project-scoping conventions, use process.env.PROJECT_ID before the single-project sentinel.

Suggested fix
 function getProjectRefFromRequest(req: AuthRequest): string {
-  return req.projectId ?? SINGLE_TENANT_PROJECT_KEY;
+  return req.projectId ?? process.env.PROJECT_ID ?? SINGLE_TENANT_PROJECT_KEY;
 }

Based on learnings, "derive the project ID from req.projectId ... or from process.env.PROJECT_ID (self-hosted). Use 'default' only as an intentional fallback for single-project setups."

🤖 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 `@backend/src/api/routes/config/apply.ts` around lines 14 - 16, The
getProjectRefFromRequest function currently returns req.projectId or
SINGLE_TENANT_PROJECT_KEY; change it to prefer an env-backed project by
returning req.projectId ?? process.env.PROJECT_ID ?? SINGLE_TENANT_PROJECT_KEY
so that process.env.PROJECT_ID is used for self-hosted deployments and
SINGLE_TENANT_PROJECT_KEY ('default') remains only the last-resort sentinel.
🤖 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 `@backend/src/api/routes/config/apply.ts`:
- Around line 38-40: The chained response call is misformatted for Prettier;
replace the multi-line chain with a single properly punctuated statement (e.g.,
return res.status(400).json({ error: 'config_validation', message: err.message,
path: err.path });) so the res.status(...).json(...) call is on one line with
proper semicolon/return to satisfy formatting/lint rules in the apply handler.
- Around line 24-33: The route is currently trusting body.dry_run and body.prune
which can be non-boolean (e.g. "false") and incorrectly enable behaviors; add a
Zod schema to validate/parse req.body (ensuring config?: unknown, dry_run?:
boolean, prune?: boolean) and use schema.parse or safeParse to coerce/validate
values before calling withConfigApplyLock/applyConfig so you always pass actual
booleans (defaulting to false when absent or invalid); reference the request
handler that reads body and the applyConfig call to replace the ad-hoc casting
with the validated result.

In `@backend/src/services/config/apply.test.ts`:
- Around line 33-44: The test title promises applied=true but doesn't assert it;
update the test that calls applyConfig (in
backend/src/services/config/apply.test.ts) to capture the return value from
applyConfig(...) and add an assertion that the returned object's applied
property is true (e.g., expect(result.applied).toBe(true)), keeping the existing
check that setAuthSettings was called with the expected additionalRedirectUrls;
target the applyConfig invocation and the setAuthSettings mock in your change.

In `@backend/src/services/config/apply.ts`:
- Around line 27-48: The current apply logic may persist auth via
setAuthSettings() before bucket changes (upsertBucket/deleteBucket) run, causing
partial updates on failure; modify apply to run all changes inside a single
database transaction (or use an existing transactional API) so setAuthSettings,
upsertBucket and deleteBucket are executed atomically, or if transactions are
unavailable implement compensating rollback that reverses setAuthSettings and
any upsertBucket/deleteBucket effects when a later step fails; update code paths
that call setAuthSettings, upsertBucket, and deleteBucket to accept/participate
in the transaction (or return undo information) and ensure plan.changes
iteration either runs inside that transaction scope or triggers rollback on
error.
- Around line 23-25: The response sets applied: true even when no mutation
happens (e.g., all plan.changes are remove operations with kept === true and
prune is disabled) — update the logic that computes the returned applied flag to
reflect whether any actual mutation occurred: when preparing the result in apply
(use the same scope where input.dry_run, plan.changes and the final applied
value are set) compute a boolean like mutated = plan.changes.some(c => !(c.type
=== 'remove' && c.kept === true)) (and treat dry_run as always false for
mutation detection), then return { plan, applied: mutated } (preserving the
existing early return for input.dry_run or empty changes but ensure
applied=false in those cases).

In `@docs/agent-docs/config-as-code.md`:
- Around line 68-71: The docs overstate concurrency: clarify that taking a
Postgres advisory lock for each `apply` keyed on `(project, "config_apply")`
only enables cross-project parallel `apply` calls in multi-tenant/project-scoped
deployments; in single-tenant OSS installs we serialize all applies on a fixed
project key unless `req.projectId` is present. Update the Concurrency section to
make this conditional (e.g., “in multi-tenant/project-scoped deployments (when
`req.projectId` is set) calls for different projects run in parallel; otherwise
all applies are serialized on a single project key”).

---

Nitpick comments:
In `@backend/src/api/routes/config/apply.ts`:
- Around line 14-16: The getProjectRefFromRequest function currently returns
req.projectId or SINGLE_TENANT_PROJECT_KEY; change it to prefer an env-backed
project by returning req.projectId ?? process.env.PROJECT_ID ??
SINGLE_TENANT_PROJECT_KEY so that process.env.PROJECT_ID is used for self-hosted
deployments and SINGLE_TENANT_PROJECT_KEY ('default') remains only the
last-resort sentinel.

In `@backend/src/services/config/diff.ts`:
- Around line 106-117: deepEqual currently only compares primitives and arrays
and returns false for plain objects; update the deepEqual function to also
handle plain object comparison by: treating null separately, detecting non-array
objects (typeof === "object" && !Array.isArray), comparing their key sets length
and presence, and recursively calling deepEqual on each corresponding property
value (use the existing array handling for arrays and === for primitives as
fallbacks). Ensure the function still short-circuits identical references (a ===
b) and preserves array handling in deepEqual.

In `@backend/src/services/config/schema.ts`:
- Around line 32-51: Replace the hand-rolled validation in validateConfig with a
Zod schema: add imports from z, define BucketConfigSchema, StorageConfigSchema,
AuthConfigSchema and InsforgeConfigSchema using z.object(...).strict() and
optional() for optional fields, derive the InsforgeConfig type with
z.infer<typeof InsforgeConfigSchema>, and update validateConfig to simply return
InsforgeConfigSchema.parse(input); remove the manual checks around
project_id/auth/storage and stop calling validateAuth/validateStorage (instead
embed their schemas or reuse them) so unknown keys are rejected and Zod error
messages are returned.

In `@backend/src/services/storage/buckets.ts`:
- Around line 26-34: The upsertBucket function uses a check-then-act pattern
(bucketExists → createBucket) that is vulnerable to a TOCTOU race; make it
explicit and safe by either adding a short comment above upsertBucket that
callers must hold the withConfigApplyLock when calling it, or make the function
itself resilient by wrapping the createBucket call in a try-catch that treats an
"already exists" error as success and proceeds to update visibility (use the
existing bucketExists, createBucket, updateBucketVisibility symbols for
context); prefer the try-catch approach if you want internal safety, otherwise
document the lock requirement clearly.
🪄 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: cb6788b7-9f1a-4660-b863-348c3c1f6b69

📥 Commits

Reviewing files that changed from the base of the PR and between 52633c3 and db13c5d.

📒 Files selected for processing (15)
  • backend/src/api/routes/config/apply.ts
  • backend/src/api/routes/config/get.ts
  • backend/src/api/routes/config/index.routes.ts
  • backend/src/server.ts
  • backend/src/services/auth/settings.ts
  • backend/src/services/config/apply.test.ts
  • backend/src/services/config/apply.ts
  • backend/src/services/config/diff.test.ts
  • backend/src/services/config/diff.ts
  • backend/src/services/config/lock.test.ts
  • backend/src/services/config/lock.ts
  • backend/src/services/config/read.ts
  • backend/src/services/config/schema.ts
  • backend/src/services/storage/buckets.ts
  • docs/agent-docs/config-as-code.md

Comment on lines +24 to +33
const body = req.body as { config?: unknown; dry_run?: boolean; prune?: boolean };

try {
const pool = DatabaseManager.getInstance().getPool();
const result = await withConfigApplyLock(pool, projectRef, () =>
applyConfig({
config: body.config,
dry_run: body.dry_run ?? false,
prune: body.prune ?? false,
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate dry_run and prune as booleans before apply.

Lines 31-32 accept any non-null value; "false" (string) is truthy and can incorrectly enable dry-run/prune behavior. This can cause unintended bucket deletions.

Suggested fix
 import type { Request, Response, NextFunction } from 'express';
+import { z } from 'zod';
 import { applyConfig } from '@/services/config/apply.js';
 ...
+const applyRequestSchema = z.object({
+  config: z.unknown(),
+  dry_run: z.boolean().optional(),
+  prune: z.boolean().optional(),
+});
+
 export async function handleApplyConfig(
   req: Request,
   res: Response,
   next: NextFunction
 ): Promise<void> {
   const projectRef = getProjectRefFromRequest(req as AuthRequest);
-  const body = req.body as { config?: unknown; dry_run?: boolean; prune?: boolean };
+  const parsed = applyRequestSchema.safeParse(req.body);
+  if (!parsed.success) {
+    res.status(400).json({ error: 'invalid_request', message: parsed.error.message });
+    return;
+  }
+  const body = parsed.data;

As per coding guidelines, "Use Zod schemas for request/response validation in backend".

🤖 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 `@backend/src/api/routes/config/apply.ts` around lines 24 - 33, The route is
currently trusting body.dry_run and body.prune which can be non-boolean (e.g.
"false") and incorrectly enable behaviors; add a Zod schema to validate/parse
req.body (ensuring config?: unknown, dry_run?: boolean, prune?: boolean) and use
schema.parse or safeParse to coerce/validate values before calling
withConfigApplyLock/applyConfig so you always pass actual booleans (defaulting
to false when absent or invalid); reference the request handler that reads body
and the applyConfig call to replace the ad-hoc casting with the validated
result.

Comment on lines +38 to +40
res
.status(400)
.json({ error: 'config_validation', message: err.message, path: err.path });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prettier formatting at Lines 38-40 is failing CI.

This style mismatch is currently blocking lint-and-format checks.

Suggested fix
-      res
-        .status(400)
-        .json({ error: 'config_validation', message: err.message, path: err.path });
+      res.status(400).json({ error: 'config_validation', message: err.message, path: err.path });
📝 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
res
.status(400)
.json({ error: 'config_validation', message: err.message, path: err.path });
res.status(400).json({ error: 'config_validation', message: err.message, path: err.path });
🧰 Tools
🪛 ESLint

[error] 38-40: Replace ⏎········.status(400)⏎········ with .status(400)

(prettier/prettier)

🪛 GitHub Actions: Lint and Format Check / 0_lint-and-format.txt

[error] 38-38: prettier/prettier: Replace ⏎········.status(400)⏎········ with .status(400) (step: npx eslint backend/src/api/routes/config/apply.ts)

🪛 GitHub Actions: Lint and Format Check / lint-and-format

[error] 38-38: Prettier formatting error from ESLint: Replace ⏎········.status(400)⏎········ with .status(400) prettier/prettier. Command: npx eslint backend/src/api/routes/config/apply.ts

🪛 GitHub Check: lint-and-format

[failure] 38-38:
Replace ⏎········.status(400)⏎········ with .status(400)

🤖 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 `@backend/src/api/routes/config/apply.ts` around lines 38 - 40, The chained
response call is misformatted for Prettier; replace the multi-line chain with a
single properly punctuated statement (e.g., return res.status(400).json({ error:
'config_validation', message: err.message, path: err.path });) so the
res.status(...).json(...) call is on one line with proper semicolon/return to
satisfy formatting/lint rules in the apply handler.

Comment on lines +33 to +44
it('applies modifications and returns applied=true', async () => {
const { setAuthSettings } = await import('../auth/settings.js');
vi.clearAllMocks();
await applyConfig({
config: { auth: { additional_redirect_urls: ['http://b'] } },
dry_run: false,
prune: false,
});
expect(setAuthSettings).toHaveBeenCalledWith(
expect.objectContaining({ additionalRedirectUrls: ['http://b'] }),
);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test title promises applied=true, but it isn’t asserted.

Line 33 says this case verifies applied=true, but the test only checks setAuthSettings. Please assert the returned flag explicitly.

Suggested fix
   it('applies modifications and returns applied=true', async () => {
     const { setAuthSettings } = await import('../auth/settings.js');
     vi.clearAllMocks();
-    await applyConfig({
+    const result = await applyConfig({
       config: { auth: { additional_redirect_urls: ['http://b'] } },
       dry_run: false,
       prune: false,
     });
+    expect(result.applied).toBe(true);
     expect(setAuthSettings).toHaveBeenCalledWith(
       expect.objectContaining({ additionalRedirectUrls: ['http://b'] }),
     );
   });
📝 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
it('applies modifications and returns applied=true', async () => {
const { setAuthSettings } = await import('../auth/settings.js');
vi.clearAllMocks();
await applyConfig({
config: { auth: { additional_redirect_urls: ['http://b'] } },
dry_run: false,
prune: false,
});
expect(setAuthSettings).toHaveBeenCalledWith(
expect.objectContaining({ additionalRedirectUrls: ['http://b'] }),
);
});
it('applies modifications and returns applied=true', async () => {
const { setAuthSettings } = await import('../auth/settings.js');
vi.clearAllMocks();
const result = await applyConfig({
config: { auth: { additional_redirect_urls: ['http://b'] } },
dry_run: false,
prune: false,
});
expect(result.applied).toBe(true);
expect(setAuthSettings).toHaveBeenCalledWith(
expect.objectContaining({ additionalRedirectUrls: ['http://b'] }),
);
});
🤖 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 `@backend/src/services/config/apply.test.ts` around lines 33 - 44, The test
title promises applied=true but doesn't assert it; update the test that calls
applyConfig (in backend/src/services/config/apply.test.ts) to capture the return
value from applyConfig(...) and add an assertion that the returned object's
applied property is true (e.g., expect(result.applied).toBe(true)), keeping the
existing check that setAuthSettings was called with the expected
additionalRedirectUrls; target the applyConfig invocation and the
setAuthSettings mock in your change.

Comment on lines +23 to +25
if (input.dry_run || plan.changes.length === 0) {
return { plan, applied: false };
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

applied can be true even when nothing was mutated.

Line 50 returns applied: true for plans that only contain remove changes with kept: true (prune disabled), but Lines 45-47 intentionally skip mutation. This makes the response misleading.

Suggested fix
 export async function applyConfig(input: ApplyInput): Promise<ApplyResult> {
   const file = validateConfig(input.config);
   const live = await readLiveConfig();
   const plan = diffConfig({ live, file, prune: input.prune });
+  const actionableChanges = plan.changes.filter(
+    (change) =>
+      !(
+        change.section === 'storage.buckets' &&
+        change.op === 'remove' &&
+        change.kept
+      )
+  );

-  if (input.dry_run || plan.changes.length === 0) {
+  if (input.dry_run || actionableChanges.length === 0) {
     return { plan, applied: false };
   }

-  const authChanges = plan.changes.filter((c) => c.section === 'auth');
+  const authChanges = actionableChanges.filter((c) => c.section === 'auth');
   if (authChanges.length > 0 && file.auth) {
     await setAuthSettings({
       additionalRedirectUrls:
         file.auth.additional_redirect_urls ?? live.auth?.additional_redirect_urls ?? [],
     });
   }

-  for (const c of plan.changes) {
+  for (const c of actionableChanges) {

Also applies to: 45-50

🤖 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 `@backend/src/services/config/apply.ts` around lines 23 - 25, The response sets
applied: true even when no mutation happens (e.g., all plan.changes are remove
operations with kept === true and prune is disabled) — update the logic that
computes the returned applied flag to reflect whether any actual mutation
occurred: when preparing the result in apply (use the same scope where
input.dry_run, plan.changes and the final applied value are set) compute a
boolean like mutated = plan.changes.some(c => !(c.type === 'remove' && c.kept
=== true)) (and treat dry_run as always false for mutation detection), then
return { plan, applied: mutated } (preserving the existing early return for
input.dry_run or empty changes but ensure applied=false in those cases).

Comment on lines +27 to +48
// Apply auth section if any auth changes are present.
const authChanges = plan.changes.filter((c) => c.section === 'auth');
if (authChanges.length > 0 && file.auth) {
await setAuthSettings({
additionalRedirectUrls:
file.auth.additional_redirect_urls ?? live.auth?.additional_redirect_urls ?? [],
});
}

// Apply storage.buckets section.
for (const c of plan.changes) {
if (c.section !== 'storage.buckets') {
continue;
}
if (c.op === 'add') {
await upsertBucket({ name: c.key, public: c.value.public ?? false });
} else if (c.op === 'modify') {
await upsertBucket({ name: c.key, public: c.to });
} else if (c.op === 'remove' && !c.kept) {
await deleteBucket(c.key);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Non-atomic apply can leave partial state on failure.

Line 30 can persist auth changes before a later bucket mutation fails on Lines 42-47, which leaves the system partially updated despite an error response. Please make the apply phase atomic (single DB transaction where possible) or add compensating rollback logic per applied step.

As per coding guidelines, "Implement proper database transaction handling".

🤖 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 `@backend/src/services/config/apply.ts` around lines 27 - 48, The current apply
logic may persist auth via setAuthSettings() before bucket changes
(upsertBucket/deleteBucket) run, causing partial updates on failure; modify
apply to run all changes inside a single database transaction (or use an
existing transactional API) so setAuthSettings, upsertBucket and deleteBucket
are executed atomically, or if transactions are unavailable implement
compensating rollback that reverses setAuthSettings and any
upsertBucket/deleteBucket effects when a later step fails; update code paths
that call setAuthSettings, upsertBucket, and deleteBucket to accept/participate
in the transaction (or return undo information) and ensure plan.changes
iteration either runs inside that transaction scope or triggers rollback on
error.

Comment on lines +68 to +71
## Concurrency

Each `apply` takes a Postgres advisory lock keyed on `(project, "config_apply")`. Two concurrent `apply` calls serialize; calls for different projects run in parallel.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify single-tenant lock behavior to avoid overstating parallelism.

Line 70 currently implies cross-project parallel applies unconditionally, but current OSS behavior serializes all applies on a fixed project key unless req.projectId is present. Please make this conditional in the doc (e.g., “in multi-tenant/project-scoped deployments”).

🤖 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 `@docs/agent-docs/config-as-code.md` around lines 68 - 71, The docs overstate
concurrency: clarify that taking a Postgres advisory lock for each `apply` keyed
on `(project, "config_apply")` only enables cross-project parallel `apply` calls
in multi-tenant/project-scoped deployments; in single-tenant OSS installs we
serialize all applies on a fixed project key unless `req.projectId` is present.
Update the Concurrency section to make this conditional (e.g., “in
multi-tenant/project-scoped deployments (when `req.projectId` is set) calls for
different projects run in parallel; otherwise all applies are serialized on a
single project key”).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Morty Proxy This is a proxified and sanitized view of the page, visit original site.