one clarification on the architecture: if you control the server-side WebSocket handler, the simplest path is to validate and write inside the message handler using the service role client, not the anon key. supabase Edge Functions support WebSocket connections directly: ```ts // supabase/functions/ws-ingest/index.ts import { createClient } from "@supabase/supabase-js"; const sb = createClient( Deno.env.get("SUPABASE_URL"), Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ); Deno.serve((req) => { if (req.headers.get("upgrade") !== "websocket") { return new Response("expected websocket", { status: 400 }); } const { socket, response } = Deno.upgradeWebSocket(req); socket.onmessage = async (event) => { let payload; try { payload = JSON.parse(event.data); } catch { socket.send(JSON.stringify({ ok: false, error: "invalid json" })); return; } const { error } = await sb.from("your_table").insert(payload); if (error) { socket.send(JSON.stringify({ ok: false, error: error.message })); } else { socket.send(JSON.stringify({ ok: true })); } }; return response; }); ``` the client connects to wss://your-project.supabase.co/functions/v1/ws-ingest. you do authentication in the onmessage handler before the insert, either by parsing a token from the first message or from the request headers on upgrade.
Good point on the operator resolution angle. With proconfig NULL confirmed in the catalog, the alter function set search_path approach is the lowest-risk test before touching the function body. If pinning the path does not change the result, the EXPLAIN VERBOSE comparison between the function call and the inlined SELECT should show the difference immediately. The inlining path is where SECURITY INVOKER combined with RLS can also flip the result, so both angles are worth checking in order.
Frontend hosting for SSR frameworks like Next.js would require a substantial amount of additional infrastructure that is quite different from what Supabase runs today: a build system, edge CDN, cache invalidation, and runtime environments for server-rendered pages. That is essentially what Vercel and Netlify have spent years building, so it is a large addition to the platform scope. A simpler version of what you are asking already exists. Supabase Storage supports serving static files from a public bucket with a custom domain, which covers Vite and Create React App SPAs without any additional hosting service. You build locally or in CI, upload the dist folder to a public bucket, and point your domain at the storage URL. For Next.js specifically, the native Vercel integration with Supabase is fairly tight: it handles connection string injection, connection pooling via the Supabase integration, and environment variable sync. The free tiers of both platforms together cover most hobby and early-stage projects without adding billing complexity. For those who want everything in one place, the most practical near-term option is to deploy Next.js to a Supabase Edge Function using the Next.js edge runtime adapter, which keeps compute and database in the same region and under the same billing account. It is more setup than a managed platform but it achieves the co-location goal. Worth exploring if consolidation is the main driver.
Running self-hosted Supabase on a single Hetzner VPS using the official docker-compose setup. The stack has been in production for just over a year serving a small internal tool for a team of around 15 people. Deployment is straightforward: pull the repo, edit the .env file, run docker compose up. Updates are just a docker compose pull followed by a restart. No Kubernetes, no managed database service, no extra tooling. What works well is the REST API and auth layer. GoTrue and PostgREST are essentially zero-maintenance once the initial config is done. Row-level security policies work exactly the same as on the cloud platform, so the behavior is predictable. Realtime has also been solid for our use case (presence and broadcast on a few channels). Storage works fine for small files. The friction points are Postgres major version upgrades and the Kong configuration. Upgrading from Postgres 14 to 15 required a manual dump and restore because there is no built-in pg_upgrade path in the compose setup. For a small database this is manageable but it is nerve-racking. Kong config is verbose and easy to break if you need to add custom routes or middleware. The one thing that would make the biggest difference is a first-class major version upgrade path for Postgres built into the compose tooling. Something equivalent to what the Supabase CLI does for hosted projects. Even a documented, tested dump-and-restore script that handles the volume names and config migration correctly would help a lot.
The pattern you are describing (TCP connects, L7 handshake never completes, Management API works) is consistent with the Supavisor process behind the pooler endpoint being in a degraded state while the underlying Postgres engine is fine. The Management API goes through a different path than the pooler, which is why one works and the other does not. A few things to try before escalating to support. First, try port 5433 instead of 5432. Port 5432 is transaction mode, 5433 is session mode. They hit different Supavisor configurations and occasionally one is degraded while the other is not. Second, try making the SSL mode explicit in your connection string. Supavisor sometimes behaves differently when the client does not negotiate SSL upfront. Add `?sslmode=require` to your DATABASE_URL and retry. Third, try the direct database connection string (not the pooler) to confirm whether Prisma migrations can go through the direct connection. The direct connection bypasses Supavisor entirely and connects to Postgres on port 5432 at the db.your-ref.supabase.co host. For migrations this is actually the recommended approach since Prisma db push and migrate use DDL statements that do not work well with transaction-mode pooling. If none of these help, open a support ticket at supabase.com/support with your project ref, the exact timestamps of the failed connection attempts, and a note that the project was restarted on 2026-07-07 without resolving it. The team will need to look at the Supavisor instance directly.
The "No backups found" error when resuming a paused free-tier project is a server-side infrastructure state issue, not something you can resolve from the browser side. Refreshing, incognito, and different browsers will not help because the error originates on Supabase infrastructure, not in your client. Your data is intact as the message states. The pause mechanism keeps your Postgres data on disk, it does not delete it. The error means the resume flow cannot find the expected backup snapshot to boot from, which leaves the project stuck in a paused state. The fastest path to getting it unstuck is to open a support ticket at supabase.com/support and include your project reference ID. The support team can manually trigger the resume or restore the project from their side. Include the exact error text and a note that browser-side workarounds have already been tried so the ticket goes straight to the right queue. If you are on the free plan, note that free projects pause after a period of inactivity and the resume process depends on a recent backup snapshot being available. If the project has been paused for a long time, that snapshot window may have expired, which is another reason the support team needs to handle this manually.
pgPointcloud is not currently available on Supabase Cloud. PostGIS is supported but pgPointcloud requires additional shared libraries compiled into Postgres that the managed platform does not include at the moment. For LiDAR work on Supabase today there are two practical paths. The first is to store the raw point cloud data in Supabase Storage as LAZ or LAS files and process them externally with a tool like PDAL or libLAS. You keep the metadata and derived attributes (bounding box, point count, classification statistics) in a Postgres table alongside a PostGIS geometry column for spatial indexing, and retrieve the actual point data from Storage on demand. This pattern works well for read-heavy workflows where you query on metadata and then fetch the raw file. The second is to self-host Supabase with a custom Postgres image that includes pgPointcloud. The self-hosting documentation at supabase.com/docs/guides/self-hosting describes how to extend the Postgres image. You would build pgPointcloud from source, add it to the image, and run it alongside the standard Supabase stack. More operational overhead, but you get the full extension. I would encourage filing a GitHub issue requesting pgPointcloud as a managed extension. The more concrete use cases and upvotes an extension request has, the easier it is for the team to prioritize adding it to the managed platform.
This is a reasonable UX request and the workaround until a dashboard setting exists is straightforward. For display purposes you can create a view that casts your timestamptz columns to your local timezone. For example: ```sql CREATE VIEW orders_kst AS SELECT id, created_at AT TIME ZONE 'Asia/Seoul' AS created_at_kst, amount FROM orders; ``` The Table Editor will show that view, and the timestamps will display in KST while the underlying table still stores UTC. The stored value is never changed, so queries from your application that read the base table are unaffected. A second option if you do not want to create views for every table is to use a generated column: ```sql ALTER TABLE orders ADD COLUMN created_at_kst timestamptz GENERATED ALWAYS AS (created_at AT TIME ZONE 'Asia/Seoul') STORED; ``` Neither of these replaces a proper dashboard-level timezone preference, but they make the Table Editor readable right now without waiting for that feature. I would suggest adding this as a GitHub issue with a link back to this discussion so it gets tracked on the roadmap.
This is an interesting direction, and worth thinking through the security model carefully before building it. The main concern with a natural language to SQL endpoint is that the generated SQL is hard to sandbox. Even with row-level security enabled, a poorly worded query could return much more data than intended, or the translation could produce queries that bypass the intent of your RLS policies. You would want to restrict this to SELECT only by default, validate that the generated SQL touches only the tables the caller has access to, and probably run it through EXPLAIN first to catch anything with a catastrophic estimated cost. For now, the cleanest way to approximate this is with a Supabase Edge Function that calls an LLM, passes your schema as context, generates the SQL, validates it server-side, and then runs it with the anon or service role key depending on your auth model. That keeps the AI layer in your control and lets you add whatever guardrails you need before executing. The SQL editor in the Supabase dashboard already has an AI assist feature that does exactly this for the dashboard user, so the primitives are there. A dedicated public API endpoint for this would be useful, but it would need a schema-aware permission model that does not exist yet. Filing this as a feature request in the GitHub repo with a concrete description of the permission model you have in mind would help the team scope it.
This error appears in the Postgres logs when the Supabase dashboard polls for migration history and the supabase_migrations schema has not been initialized. It is a dashboard polling artifact and does not mean your migrations are broken. It happens most often in three situations: (1) a project that was created before the migration tracking schema was introduced, (2) a self-hosted instance where the initial migration setup step was skipped, or (3) a project restored from a pg_dump that did not include the supabase_migrations schema. To initialize the schema: CREATE SCHEMA IF NOT EXISTS supabase_migrations; CREATE TABLE IF NOT EXISTS supabase_migrations.schema_migrations ( version text PRIMARY KEY, statements text[], name text ); After creating the table the polling error disappears from the logs. If you are using the Supabase CLI, running supabase db push against a linked project also creates this schema automatically.
Good feature request. The --with-data flag is available in the CLI today (supabase db branch create --with-data) but the GitHub integration does not expose it yet. You can work around this by running the CLI step as part of your CI workflow immediately after the branch is created by the integration. Add a step in your GitHub Actions workflow that runs after the Supabase branch is created: - name: Seed branch with data env: SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} run: | BRANCH_NAME="${{ github.head_ref }}" supabase db branch create "$BRANCH_NAME" --with-data \ --project-ref ${{ secrets.SUPABASE_PROJECT_ID }} 2>/dev/null || true The 2>/dev/null || true handles the case where the branch was already created by the integration. This gives you data seeding on every PR branch until the toggle lands in the UI. Upvoted the feature request.
There is no bulk-delete button in the SQL editor UI for private queries on the free plan, but you can remove them through the Supabase Management API. First, get your project ref and a personal access token from app.supabase.com/account/tokens. Then list your snippets: curl -s https://api.supabase.com/v1/projects/{project-ref}/snippets \ -H "Authorization: Bearer YOUR_TOKEN" | jq '.[].id' Delete each snippet by id: curl -X DELETE https://api.supabase.com/v1/projects/{project-ref}/snippets/{snippet-id} \ -H "Authorization: Bearer YOUR_TOKEN" To delete all of them in one go with bash: IDS=$(curl -s https://api.supabase.com/v1/projects/{project-ref}/snippets \ -H "Authorization: Bearer YOUR_TOKEN" | jq -r '.[].id') for id in $IDS; do curl -X DELETE https://api.supabase.com/v1/projects/{project-ref}/snippets/$id \ -H "Authorization: Bearer YOUR_TOKEN" done Replace {project-ref} with your project reference (found in project settings). 449 queries will take about a minute to delete this way.
The article describes the problem well. Here are the three patterns that fix it in practice. Pattern 1. Use auth.uid() directly instead of querying another table. Instead of checking a roles table inside your policy: CREATE POLICY bad_policy ON documents FOR SELECT USING ( EXISTS (SELECT 1 FROM user_roles WHERE user_id = auth.uid() AND role = 'admin') ); Check the JWT claims if you store the role there: CREATE POLICY good_policy ON documents FOR SELECT USING (auth.jwt() ->> 'role' = 'admin'); Pattern 2. Create a SECURITY DEFINER helper function. CREATE OR REPLACE FUNCTION public.current_user_role() RETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT role FROM user_roles WHERE user_id = auth.uid() LIMIT 1; $$; CREATE POLICY role_policy ON documents FOR SELECT USING (public.current_user_role() = 'admin'); The SECURITY DEFINER function runs as the owner and bypasses RLS on user_roles, breaking the cycle. Pattern 3. Grant the anon and authenticated roles SELECT on the lookup table and disable RLS on it, since the table contains no sensitive per-user data.
The Security Advisor warning on public.spatial_ref_sys is a false positive in the context of PostGIS. The table is a read-only reference table shipped by the PostGIS extension and contains coordinate system definitions. No user data lives in it. You have two practical options. Option 1. Add a permissive SELECT policy and enable RLS. This silences the advisor and is technically correct since the table is public reference data: ALTER TABLE public.spatial_ref_sys ENABLE ROW LEVEL SECURITY; CREATE POLICY spatial_ref_sys_read ON public.spatial_ref_sys FOR SELECT USING (true); Option 2. Move PostGIS to its own schema (recommended for new projects). Install the extension into an extensions schema so it is isolated from your application tables: CREATE SCHEMA IF NOT EXISTS extensions; CREATE EXTENSION IF NOT EXISTS postgis SCHEMA extensions; Then reference geometries as extensions.geometry, etc. The advisor only flags tables in the public schema. For existing projects option 1 is simpler and carries no risk since the table is insert-protected by the extension itself. Neither option changes the behavior of your application.
The behavior you are seeing is a known PostgreSQL quirk with STABLE sql-language functions under RLS. When PostgreSQL plans a STABLE LANGUAGE sql function it may inline the body into the calling query and re-evaluate security context at that point. With SECURITY INVOKER and RLS enabled, the planner can see a different row-level security check depending on whether the function body is inlined or executed as a standalone call. This is why the raw SELECT, the plpgsql wrapper, and the inlined form all return 3 rows while the sql function returns 0: the planner inlines the sql function body and the RLS policy evaluation changes in that context. The most reliable fix is to mark the function SECURITY DEFINER and add it to a non-public search_path: CREATE OR REPLACE FUNCTION public.match_kb_chunks(query_embedding vector, match_count integer) RETURNS SETOF your_table LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT * FROM your_table ORDER BY embedding <=> query_embedding LIMIT match_count; $$; This prevents the planner from inlining across the security boundary and the function runs with the owner role, bypassing RLS for the match itself. If you want RLS to still apply, keep SECURITY INVOKER and switch to LANGUAGE plpgsql, which PostgreSQL never inlines.