Quick Start
Before reading, we recommend first reviewing the PG Mode Overview to understand the relationship between PostgreSQL, authentication, cloud storage, and the permission model.
This article uses a minimal todo app as an example to demonstrate the complete flow in a CloudBase PostgreSQL database — from create table → enable RLS → authorize → write policy → SDK call — helping you get a secure multi-user data access chain working in 5–10 minutes.
Prerequisites
- Have created a CloudBase environment in PG mode and recorded:
- Environment ID (e.g.,
pg-test-3gxmdbdb580ecfd1) - Publishable Key (used by the frontend, corresponds to the
anonrole) - API Key (used by the backend, corresponds to
service_role, must never be exposed to the frontend)
- Environment ID (e.g.,
- For the three roles and their keys, see PG: Authentication. For the two-tier permission model, see Architecture and Permission Model.
Publishable Key / API Key can be created in the console (or via the CreateApiKey YUNAPI).
Step 1: Create a Table
Create a todos table. You can use the SQL Editor (Console → Database → SQL Editor) to execute directly:
CREATE TABLE public.todos (
id bigserial PRIMARY KEY,
title varchar(255) NOT NULL,
is_completed boolean NOT NULL DEFAULT false,
-- ⭐ owner_id is automatically read from the JWT as the current user ID
owner_id varchar(64) NOT NULL
DEFAULT (current_setting('request.jwt.claims', true)::json->>'sub'),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_todos_owner_id ON public.todos(owner_id);
Key point: The default value of
owner_idis bound to the JWTsub. The frontend does not need to passowner_id, preventing identity forgery at the source.
Step 2: Enable RLS
ALTER TABLE public.todos ENABLE ROW LEVEL SECURITY;
After enabling RLS, if no Policy is configured, all non-service_role users cannot access any data (default deny). Continue to the next step.
Step 3: Grant Table-Level Permissions (GRANT)
Table-level GRANT controls what kinds of operations each role can perform on the table:
-- Anonymous users: no permissions granted (unauthenticated users cannot operate personal todos)
-- Authenticated users: can read and write their own todos
GRANT SELECT, INSERT, UPDATE, DELETE ON public.todos TO authenticated;
GRANT USAGE, SELECT ON SEQUENCE public.todos_id_seq TO authenticated;
-- Admin: full permissions
GRANT ALL ON public.todos TO service_role;
GRANT USAGE, SELECT ON SEQUENCE public.todos_id_seq TO service_role;
Step 4: Create RLS Policies
Row-level Policies control which specific rows each role can operate on:
-- SELECT: authenticated users can only read their own todos
CREATE POLICY todos_select_own ON public.todos
FOR SELECT TO authenticated
USING (owner_id = (current_setting('request.jwt.claims', true)::json->>'sub'));
-- INSERT: authenticated users can only write records where owner_id = themselves
CREATE POLICY todos_insert_own ON public.todos
FOR INSERT TO authenticated
WITH CHECK (owner_id = (current_setting('request.jwt.claims', true)::json->>'sub'));
-- UPDATE: authenticated users can only update their own records, and cannot change owner_id to someone else
CREATE POLICY todos_update_own ON public.todos
FOR UPDATE TO authenticated
USING (owner_id = (current_setting('request.jwt.claims', true)::json->>'sub'))
WITH CHECK (owner_id = (current_setting('request.jwt.claims', true)::json->>'sub'));
-- DELETE: authenticated users can only delete their own records
CREATE POLICY todos_delete_own ON public.todos
FOR DELETE TO authenticated
USING (owner_id = (current_setting('request.jwt.claims', true)::json->>'sub'));
The SQL above can be executed via the cloud API ExecutePGSql (suitable for CI/CD). Note that some DDL statements under the API path need to be wrapped with DO LANGUAGE plpgsql $$ BEGIN EXECUTE '...'; END $$ and retried. This anonymous code block depends on BEGIN and dynamic EXECUTE. See Architecture and Permission Model - ExecutePGSql for details.
Step 5: Read and Write Data via SDK
Install
npm install @cloudbase/js-sdk
Usage
import cloudbase from '@cloudbase/js-sdk';
// You can also pass accessKey: '<Publishable Key>', so that unauthenticated calls use the anon identity
const app = cloudbase.init({ env: '<CloudBase environment ID>' });
const auth = app.auth;
// 1. Sign in (anonymous / password / third-party all work; anonymous shown here)
// After anonymous sign-in, the JWT has role=anon but carries a real sub, which can be used as the owner_id attribution field
await auth.signInAnonymously();
// 2. Access the PG database
const db = app.rdb();
// Query: RLS filters automatically, returning only the current user's todos
const { data, error } = await db
.from('todos')
.select('*')
.eq('is_completed', false);
// Insert: owner_id is auto-filled by the database default value as the JWT sub
await db.from('todos').insert({ title: 'Write a document' });
// Update: RLS only allows updating your own
await db.from('todos').update({ is_completed: true }).eq('id', 1);
// Delete: RLS only allows deleting your own
await db.from('todos').delete().eq('id', 1);
app.authis a property, not a method (do not writeapp.auth())- Password sign-in uses
auth.signInWithPassword({ username, password }) - For detailed APIs, see JS SDK - PostgreSQL Database and JS SDK - Authentication
Step 6: Use the REST API Directly (Optional)
For scenarios without an SDK, you can call the PostgREST RESTful API directly:
# Sign in
curl -X POST "https://<envId>.api.tcloudbasegateway.com/auth/v1/signin/anonymously" \
-H "Content-Type: application/json" \
-d '{}'
# Response: { "access_token": "eyJhbG...", ... }
# Query
curl "https://<envId>.api.tcloudbasegateway.com/v1/rdb/rest/todos?select=*&is_completed=eq.false" \
-H "Authorization: Bearer <access_token>"
# Insert (owner_id omitted, auto-filled by DEFAULT)
curl -X POST "https://<envId>.api.tcloudbasegateway.com/v1/rdb/rest/todos" \
-H "Authorization: Bearer <access_token>" \
-H "Prefer: return=representation" \
-H "Content-Type: application/json" \
-d '{"title":"Write a document"}'
Verify RLS Is Working
- Unauthenticated access (no Authorization header, or called as
anon) → no permission, returns an empty array or 403 - Access after sign-in → only returns your own data
- Forge owner_id (frontend maliciously passes
"owner_id": "other-user") → rejected byWITH CHECK - Try to read another user's todos (e.g.,
?owner_id=eq.other-user) → even with the filter, theUSINGpolicy still only returns your own data
Common Pitfalls
- Enabled RLS but wrote no Policy → all non-
service_rolerequests are denied - Wrote a Policy but forgot GRANT → table-level permission check fails
- UPDATE only wrote
USING, notWITH CHECK→ user can changeowner_idto someone else and steal data - Used
API Keyin frontend code → equivalent to fully exposing all data (service_roleBYPASSRLS) - Used
serial/bigserialprimary key but forgot to grantSEQUENCEpermission → INSERT reports a permission error - Some statements error directly when DDL goes through
ExecutePGSql→ on failure, wrap withDO LANGUAGE plpgsql $$ BEGIN EXECUTE '...'; END $$and retry
Next Steps
- Framework quick starts:
- Further reading:
- PG: Authentication (includes three roles + JWT)
- PG Mode Cloud Storage
- Architecture and Permission Model
- Basic Permission Management
- PostgREST RESTful API