Skip to Content

Release Notes


v0.1.80 — 12 Jun 2026


Bug Fix — Content Detail forms now handle numeric, non-PK M2O, and input-code values correctly

Who is affected: Users editing records in Platform Studio Content Detail pages where fields include numeric inputs, M2O relations keyed by non-primary columns (for example resource_uri), or input-code interfaces for JSON/CSV.

What was wrong

  • Numeric values could be treated as string-only in parts of the input mapping.
  • M2O dropdown resolution assumed primary-key based lookup and could fail for non-PK target columns.
  • input-code handling for non-string/structured values was inconsistent, which could produce incorrect payload values.

What changed

  • Input mapping now preserves numeric behavior instead of forcing string-only assumptions.
  • M2O lookup/selection now supports filter-based resolution for non-PK targets.
  • input-code now normalizes non-string input and emits parsed JSON for structured values where applicable.

Before / After

ScenarioBefore this fixAfter this fix
Numeric field editing in Content Detail❌ Could be coerced through string-only mapping✅ Correct numeric-aware handling
M2O keyed by non-PK column❌ Selection/lookup could fail✅ Filter-based non-PK lookup works
Structured JSON in input-code❌ Inconsistent emitted values✅ Normalized + correctly parsed payload

v0.1.79 — 12 Jun 2026


Who is affected: Self-hosted deployments that set both SUPABASE_INTERNAL_URL and NEXT_PUBLIC_SUPABASE_URL to different values (for example, internal service DNS for server code and public HTTPS URL for browser access).

What was wrong

After introducing SUPABASE_INTERNAL_URL, server-side requests could fail to pick up browser auth cookies in some environments. Sessions appeared valid in the browser but were not consistently recognized by middleware/SSR code.

Root cause

Supabase SSR derives the cookie name from the URL passed to createServerClient. When server code used SUPABASE_INTERNAL_URL, that derived cookie name could differ from the browser cookie name derived from NEXT_PUBLIC_SUPABASE_URL.

What changed

Server-side clients now explicitly set the cookie name derived from NEXT_PUBLIC_SUPABASE_URL via cookieOptions.name, while still connecting to Supabase through SUPABASE_INTERNAL_URL for server-to-server traffic.

Before / After

ScenarioBefore this fixAfter this fix
Internal URL differs from public URL❌ Cookie-based SSR auth could fail intermittently✅ Cookie-based SSR auth resolves consistently
Server network path✅ Internal URL used✅ Internal URL still used
Browser auth cookie compatibility❌ Could mismatch✅ Explicitly aligned

v0.1.78 — 11 Jun 2026


Bug Fix — Added SUPABASE_INTERNAL_URL support for server-side Supabase connections

Who is affected: Operators running BuildPad in Docker/Kubernetes or private network topologies where app servers should reach Supabase over an internal hostname instead of the public endpoint.

What was wrong

Server-side code paths (API routes, middleware, session helpers, and auth flows) always used NEXT_PUBLIC_SUPABASE_URL, which is browser-facing and may route through a public load balancer.

What changed

SUPABASE_INTERNAL_URL is now supported as a server-only override across server-side Supabase and auth paths. When set, server code uses the internal URL; when absent, behavior falls back to NEXT_PUBLIC_SUPABASE_URL.

Before / After

ScenarioBefore this fixAfter this fix
API route / middleware Supabase target❌ Always public URL✅ Uses SUPABASE_INTERNAL_URL when configured
Browser Supabase target✅ Public URL✅ Still public URL
Self-hosted internal network optimization❌ Not possible without code changes✅ Supported via env config

Bug Fix — Docs site hard reload no longer breaks due to early widget script execution

Who is affected: Users browsing the docs site who encountered broken hard reload behavior while menu navigation still worked.

What was wrong

The feedback widget script could execute before hydration and mutate the DOM that React expected, causing hard reload instability.

What changed

The widget script loading strategy was changed from beforeInteractive to lazyOnload, so script execution happens after hydration.


v0.1.77 — 28 May 2026


Bug Fix — Nested M2M relations now resolve correctly at arbitrary depth

Who is affected: Anyone whose API queries traverse two or more levels of M2M relations — for example, fetching a user’s roles and then the policies assigned to each role in a single request.

What was wrong

Querying through two consecutive M2M relations returned an empty array for the second level. For example:

GET /api/users/{id}?fields=id,roles.role_id.policies.*

Returned the correct junction rows for roles, but every nested role_id object had policies: [] — the second-level M2M was silently dropped.

The same failure affected M2O → M2M chains. Querying:

GET /api/items/daas_user_roles?fields=id,role_id.policies.*

Returned a 500 with a PostgREST parse error because the query builder generated role_id(id,policies(*)), treating policies as if it were a real FK column in the database.

Root cause

Two independent bugs combined to produce the failure:

Bug 1 — Virtual alias fields on system collections were invisible. The query builder discovered alias fields (like policies on daas_roles) by looking them up in daas_fields. For system collections — daas_roles, daas_users, etc. — that table has no rows. The policies alias exists only in daas_relations (one_field = 'policies'). When daas_fields came up empty, the builder fell back to a raw PostgREST embedded select (policies(*)) which PostgREST rejected because there is no FK column by that name.

Bug 2 — Admin M2O was never post-processed. For admin users, M2O fields were fetched via PostgREST embedded joins in the SELECT string. PostgREST has no knowledge of virtual alias relations, so nested alias fields on the related collection were silently dropped. The processM2OResults post-processing path — which correctly handles virtual alias fields — was only used for non-admin users.

What changed

  • The query builder now consults daas_relations directly for any nested field that isn’t found in daas_fields, so virtual alias fields on system collections are correctly identified and routed through the relation pipeline.
  • Admin M2O fields with nested alias relations are now processed through processM2OResults instead of PostgREST embedding, consistent with how non-admin users have always been handled.
  • M2M and M2O post-processors now recursively resolve nested relations on related-collection items, enabling arbitrarily deep chains (M2M → M2M, M2O → M2M → M2O, etc.).

Before / After

Query patternBefore this fixAfter this fix
fields=roles.role_id.policies.* (M2M → M2M)policies silently empty✅ Junction rows returned
fields=roles.role_id.policies.policy.* (expand FK at level 2)❌ Empty✅ Policy objects returned
fields=role_id.policies.* on junction table (M2O → M2M)❌ 500 parse error✅ Junction rows returned
fields=*.*.* (wildcard three levels deep)❌ Level-2 M2M empty✅ Full expansion
Non-admin with permissions on all collections in chain❌ Same failures✅ Correct, with permission enforcement

Permission enforcement is unchanged

The fix does not relax any access controls. Non-admin users must still hold read permission on every collection in the chain — the junction table, the related collection, and any further-nested collection. A missing permission at any level returns [] for that field rather than an error, matching the behaviour of all other relation types.


v0.1.76 — 25 May 2026


Bug Fix — Cron jobs triggered from extensions now execute the current code

Who is affected: Anyone whose runtime extensions call services.cron.trigger() or context.services.cron.trigger() to fire a cron job.

What was wrong

Cron jobs triggered from an extension ran with the code that was compiled when the server last started. Edits made to the cron job after startup were ignored by extension-triggered runs, even hours later. Running the same job via the UI “Run Now” button or the MCP run_now action always used the current code.

Root cause

Next.js splits server-side code into multiple webpack chunks at build time. The CronManager module was evaluated separately in the startup chunk (which initialises the scheduler) and in the API-route chunk (which handles saves and manual triggers). This produced two independent CronManager instances, each with its own compiled-handler cache.

When a cron job was saved, the save API called reloadJob() on the API-route instance — updating that cache — while the startup instance (used by the scheduler and by services.cron.trigger()) retained the old compiled code from startup.

The RuntimeExtensionManager (for extensions) and the event emitter already used a globalThis singleton pattern specifically to prevent this. CronManager was missing the same treatment.

What changed

CronManager is now stored on globalThis, matching the pattern already used by RuntimeExtensionManager. All execution paths — scheduled ticks, UI/MCP triggers, and extension-triggered runs — share the same instance, so saving a cron job always updates the cache used by every caller.

Before / After

Trigger pathBefore this fixAfter this fix
Scheduled tick✅ Current code (startup cache)✅ Current code
UI “Run Now” / MCP run_now✅ Current code (API cache)✅ Current code
services.cron.trigger() from extension❌ Startup-time code (stale)✅ Current code

Improvement — Timeout field now accepts any value above the minimum

Affected pages: Extension detail, Cron job detail.

The Timeout (ms) input previously enforced a hard upper limit in the UI (30 s for extensions, 5 min for cron jobs). This cap has been removed. The field now accepts any value above its minimum — 100 ms for extensions, 1 000 ms for cron — so operators can configure whatever execution window their workloads require.


v0.1.75 — 22 May 2026


Bug Fix — Filter extensions on daas_users now correctly block user creation via POST /api/users

Affected endpoint: POST /api/users

Who is affected: Anyone who attached a filter type extension to the daas_users.items.create event and expected a rejected filter to prevent the user from being created.

What was wrong

When a filter extension threw an error (e.g. new HookError('Not allowed', 400)) the API returned a 400 response as expected — but the user was still created. Subsequent GET /api/users calls would show the new user, and their Supabase Auth entry was fully active.

Root cause

The endpoint called supabase.auth.admin.createUser() before running the filter. Supabase’s auth API fires a database trigger on success that immediately inserts the user into daas_users. By the time the filter ran, the creation was already committed and could not be reversed.

What changed

The handler now runs the filter first — before any write to Auth or the database. If the filter rejects the operation, nothing is created. Only after the filter approves the payload does the handler call auth.admin.createUser(), stamp the returned id onto the record, and persist it.

Before / After

ScenarioBefore this fixAfter this fix
Filter throws HookError(400)❌ 400 returned but user created✅ 400 returned, user not created
Filter modifies email / name❌ Original values used for auth user✅ Filter-modified values used
Filter approves payload✅ User created✅ User created

Bug Fix — POST /api/utils/import/[collection] now enforces permissions and runs extension filters

Affected endpoint: POST /api/utils/import/{collection}

Who is affected:

  • Anyone who attached a filter or action extension to items.create on a collection — those extensions were silently skipped for all imported records.
  • Anyone who relied on collection-level permission policies to restrict write access — any authenticated user could import data into any collection.

What was wrong

Extensions skipped: The endpoint wrote records directly to the database using a raw upsert, bypassing the entire extension and hook pipeline. Filter extensions, action hooks, and validation rules were never called.

No permission check: The endpoint only verified that the caller was authenticated. It did not check whether the caller had create permission on the target collection.

What changed

  • All writes now go through ItemsService, which runs the full filter and hook pipeline. A filter extension that blocks items.create will now also block import, and the per-record error is captured in the errors array in the response.
  • Non-admin users must have create permission on the target collection; permission errors return 403.
  • Scope (X-Resource-Uri) is respected, consistent with every other write endpoint.

Before / After

ScenarioBefore this fixAfter this fix
Filter extension on items.create during import❌ Silently skipped✅ Executed per record
Non-admin without create permission imports data❌ Allowed (auth check only)✅ 403 Forbidden
Import by admin✅ Worked✅ Still works
Extension error for one batch❌ N/A (bypassed)✅ Captured in errors[]; other batches continue

Behaviour change

If you were importing data as a non-admin user without an explicit create permission policy on the collection, those imports will now return 403. Add a create permission entry for the relevant role to restore access.

Additionally, the updated field has been removed from the response body — the response now contains inserted and total only.


v0.1.74 — 20 May 2026


v0.1.74 — 20 May 2026


Improvement — Platform Studio is now usable on mobile

Who is affected: Anyone using Platform Studio on a phone or small tablet.

What was wrong

On mobile widths the header wrapped and overflowed, the sidebar was stuck in icon-only mode inside the burger drawer (so navigation labels weren’t visible), the Content module had no way to switch collections without going back to the data-model page, and wide tables could not be horizontally scrolled.

What changed

  • The header now stays on a single row at every viewport width.
  • Opening the burger drawer on mobile shows the fully expanded sidebar with all navigation labels visible.
  • The drawer auto-closes on navigation, so it’s not in the way after picking a page.
  • The Content module has its own mobile collection drawer, opened from a burger icon next to the page title.
  • Wide tables scroll horizontally, and the pagination footer is sticky so it stays visible while you scroll.

Before / After

ScenarioBeforeAfter
Studio header on a phone❌ Wraps / overflows✅ Single row
Burger drawer sidebar❌ Icon-only✅ Fully expanded
Drawer after navigating❌ Stays open✅ Auto-closes
Switching collections in Content (mobile)❌ No entry point✅ Burger drawer
Wide tables on mobile❌ Clipped / unreadable✅ Horizontal scroll + sticky pagination

Improvement — Dark mode is now consistent and legible

Who is affected: Anyone using Platform Studio in dark mode.

What was wrong

Header action icons — the burger toggle, sidebar collapse button, colour scheme toggle, and scope switcher — rendered with very low contrast and were effectively invisible. The data-model field-layout cards used raw greys that inverted in dark mode, producing washed-out cards. The Buildpad logo did not swap for a dark background.

What changed

  • All header action icons now use a header-subtle style that wins against Mantine’s inline colour tokens, so they have proper contrast in both light and dark mode.
  • Field-layout cards in the data-model UI now use semantic surface tokens and render correctly in dark mode.
  • The Buildpad logo automatically swaps to its white variant in dark mode.

Before / After

ElementBefore (dark mode)After (dark mode)
Burger / sidebar / scope / theme toggle❌ Near-invisible✅ Properly visible
Field-layout cards❌ Washed-out / inverted greys✅ Correct surface colours
Buildpad logo❌ Dark logo on dark background✅ Light logo variant

Improvement — Datetime picker arrows render at the correct size

Affected fields: All datetime fields in the Content module.

The month-navigation arrows in the datetime picker are no longer oversized.


Documentation — Major accuracy pass across every public docs section

Who is affected: Anyone reading the public documentation site.

Every section of the docs has been audited against the codebase. Routes, type signatures, status values, config keys, environment variable names, and migration filenames have all been verified against the actual implementation and corrected where they had drifted.

Sections updated

  • Connect API — authentication, files, filter-rules, items, schema
  • Platform Studio — overview, data-model, content, users, roles & policies, files, settings
  • Automate — workflows, extensions, cron jobs, custom services, event hooks, versioning, logs
  • AI / MCP — available tools, connecting clients, overview
  • Self-hosting — API reference, setup guide, environment variables, runtime env whitelisting, CORS, Docker, AWS, security, migrations
  • Tutorials — headless blog, role-based access, workflow automation

Highlights worth calling out:

  • MCP client setup (macOS): the Claude Desktop config path has been corrected to ~/Library/Application Support/Claude/.... The previous ~/.claude/... path was wrong on macOS.
  • MCP client setup (Cursor): the config key in mcp.json is mcpServers — the previous mcp.servers would silently fail to register the server.
  • New MCP clients documented: Claude (Web) via custom connectors, VS Code / GitHub Copilot (.vscode/mcp.json), and Cline.
  • Self-hosting API reference now documents 8 additional endpoint groups that previously existed in the codebase but weren’t listed in the docs: activity, cron, logs, revisions, services, scope, workflow, and health.
  • Setup guide: the RLS helper function is apply_rls_to_collection (not enable_rls_for_table); the admin-user script is a shell script that requires CLI args; supabase link is required before db push.
  • Environment variables: the SMTP block has been corrected — the actual variable names are SMTP_PASSWORD, SMTP_FROM_EMAIL, SMTP_FROM_NAME, SMTP_ENABLED, and SMTP_IGNORE_TLS. NEXT_PUBLIC_APP_NAME has been removed because it does not exist.
  • Platform Studio → Settings has been rewritten: project name and URL fields do not exist in the General tab — the only field there is activity_retention_days. A previously undocumented CORS tab has been added.
  • Platform Studio → Users: access tokens are stored in plain text (not hashed), and inviting a user does not automatically send an email. These were both documented incorrectly before.
  • Workflow automation tutorial has been rewritten end to end against the actual workflow schema. Previous versions described a flat transitions array with role names; the real schema uses initial_state plus per-state commands referencing policy UUIDs. The built-in “Send Email” action shown previously did not exist — the tutorial now demonstrates the real custom-extension pattern. The transition endpoint is POST /api/workflow/transition with { workflowInstanceId, commandName }; the history endpoint is /api/workflow-instances/:id/history.

Documentation site — Feedback widget on every page

The Buildpad feedback widget is now embedded in the docs site. You can leave feedback on any docs page directly, without leaving the site.


v0.1.73 — 15 May 2026


Bug Fix — MCP-created collections now have RLS and permission policies applied

Who is affected: Anyone who used the MCP collections tool to create collections. All such collections were created without Row Level Security, making their data accessible to any authenticated Supabase session regardless of the DaaS permission model.

What was wrong

The MCP tool executed CREATE TABLE but skipped the two post-creation steps that the UI has always performed:

  1. change_table_owner — transfers table ownership so the SECURITY DEFINER function can create policies.
  2. apply_rls_to_collection — enables RLS and creates the full set of DaaS access policies (admin bypass, permission-based CRUD, and optional self-access for tables with a user_id column).

Additionally, the resource_uri performance index was not created, degrading query speed on every scoped API call for affected collections.

What changed

The MCP tool now performs all three steps immediately after CREATE TABLE, matching the UI behaviour exactly. New collections created via MCP are indistinguishable from those created via the UI with respect to RLS.

Upgrading — existing collections

A backfill migration is included: supabase/migrations/20260508000001_backfill_rls_mcp_collections.sql

It automatically applies RLS and the standard policies to every collection registered in daas_collections that currently has RLS disabled. Run it via supabase db push (or your normal migration pipeline).

Action required

If you have collections created via MCP, run the backfill migration before your next production deployment to close the permission gap.

supabase db push

Before / After

PropertyBefore this fixAfter this fix
RLS enabled on MCP-created table❌ Off✅ On
Admin bypass policy❌ Missing✅ Created
Permission-based policies (read/create/update/delete)❌ Missing✅ Created
Self-access policies (tables with user_id)❌ Missing✅ Created
resource_uri index❌ Missing✅ Created

AWS RDS environments

On RDS there is no true superuser, so the change_table_owner step may log a warning: must be able to SET ROLE "postgres". This is harmless — apply_rls_to_collection is independently SECURITY DEFINER and has sufficient privileges to enable RLS and create policies on its own.


Bug Fix — services.custom(name, { elevated: true }) now correctly elevates permissions in nested services

Who is affected: Anyone who calls services.custom('name', { elevated: true }) from a runtime extension or cron job, or anyone who experienced Custom service "name" not found or inactive errors from extensions in production.

What was wrong

Issue 1 — opts ignored: Passing { elevated: true } as the second argument to services.custom() had no effect. The nested custom service always ran with the calling user’s original permissions, so restricted writes inside the service were still blocked.

Issue 2 — production cache miss: In production Next.js builds, calling services.custom() from an extension could throw Custom service "name" not found or inactive even when the service was active. This happened because Next.js evaluates module singletons in separate chunk contexts — after a service was updated via the API, the extension runtime’s cache was empty and the lookup failed.

What changed

{ elevated: true } is now properly forwarded into the nested service’s accountability, bypassing its permission checks while the calling user is still recorded in the audit trail (user_created/user_updated and daas_activity).

The cache-miss issue is resolved with a database fallback: on a cache miss, the runtime now queries daas_custom_services directly and compiles the service on demand.

Before / After

ScenarioBefore this fixAfter this fix
services.custom('svc', { elevated: true }) from extension❌ opts ignored — policies still enforced✅ admin acc propagated — permission checks bypassed
services.custom() from extension after service update (production)❌ May throw “not found or inactive”✅ Falls back to DB lookup and compiles on demand
Triggering user in user_created / daas_activity when elevated✅ Preserved✅ Still preserved

v0.1.72 — 14 May 2026


Bug Fix — Logout now properly invalidates Bearer token sessions

Affected endpoint: POST /api/auth/logout

Who is affected: Any API client that authenticates with a Bearer token from POST /api/auth/login and relies on logout to revoke access.

What was wrong

Calling POST /api/auth/logout with a Bearer token had no effect. The token stayed fully usable — requests to GET /api/auth/user, GET /api/roles, and all other protected endpoints returned 200 after logout as if the logout never happened.

What changed

The session is now revoked server-side immediately. After a successful logout, the token is rejected across all endpoints — even before its natural JWT expiry time.

Action required for API clients

You must include the access token in the Authorization header when calling logout. Without it the server has no session to revoke.

POST /api/auth/logout Authorization: Bearer <access_token>

Before / After

ScenarioBefore this fixAfter this fix
GET /api/auth/user after logout (Bearer)✅ 200 — token still worked❌ 401 — token rejected
GET /api/roles after logout (Bearer)✅ 200 — token still worked❌ 401 — token rejected
Browser session logout✅ Worked (cookie cleared)✅ Still works (+ all sessions revoked globally)
Logout without Authorization header✅ Returned 200✅ Still returns 200 (cookie fallback)

Upgrade notes

No schema migrations or configuration changes are needed. The only change required in your client code is to include the Authorization header on logout requests if you are using Bearer token authentication.

// Before — logout without a token header (broken for Bearer clients) await fetch('/api/auth/logout', { method: 'POST' }) // After — pass the token so the session is revoked server-side await fetch('/api/auth/logout', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, })

Older releases will appear above as they are tagged.

Last updated on