Deploying PostgREST with Keycloak
Responsibility Disclaimer:
This page is an example; you are responsible for what you deploy and maintain. Expand to read more.
This page is provided for demonstration purposes only. It is not maintained as a supported Scalingo product, reference implementation or tutorial. Scalingo does not guarantee that it works continuously, remains compatible over time, or is suitable for production usage.
This demo may include external dependencies not maintained by Scalingo, customers remain responsible for:
- Validating the code and configuration they deploy against applicable security, compliance and operational requirements before any production use.
- Maintaining the applications and components they control during runtime, including monitoring releases, deploying new versions, monitoring application health and scaling as needed.
This notice does not modify or limit the applicable Agreement or Scalingo’s contractual commitments.
PostgREST is an open-source web server that automatically turns a PostgreSQL® database into a RESTful API. It allows developers to expose database tables, views, and functions directly through HTTP endpoints. PostgREST also supports filtering, pagination, relationships, and JSON responses out of the box. This makes it a lightweight and efficient option for building data-driven APIs with minimal application-layer code.
In this demo, we use PostgREST to expose a simple todos application, the same approach can be applied to other use cases.
Planning your Deployment
- PostgREST obviously requires a PostgreSQL® database. For this demo we suggest to start with a a PostgreSQL® Starter or Business 512 addon.
- PostgREST has a very small base RAM footprint. It can operate comfortably with 100 to 400 MB of RAM, depending on concurrency, payload sizes, and JSON transformations. Consequently, we suggest to start with an M container for this demo.
- To handle requests, PostgREST separates authentication from authorization:
- It authenticates the incoming HTTP request, typically by verifying a JSON Web Token. These tokens are usually generated and provided by an Identity Provider. If you already have one, please check its documentation to plug it to PostgREST. If not, an option that might be worth considering is Keycloak, for which we have a tutorial.
- It authorizes the resulting database operations, using roles, grants, views, functions, and Row-Level Security (RLS). We will setup all these hereafter.
- To keep this demo simple, we will focus on the Resource Owner Password Credentials (ROPC) grant type described in RFC 6749. With Keycloak, this flow is called Direct Access Grants. This flow mainly consists in converting user-provided credentials to a JWT.
Understanding How Authorization Works in PostgREST
- PostgREST exposes the PostgreSQL® database through an HTTP API.
- Keycloak (or any Identity Service) authenticates the user and issues a corresponding JWT.
- PostgREST verifies the JWT and extracts claims from it.
- PostgREST impersonates a PostgreSQL® role using one of the extracted claim.
- PostgreSQL® RLS distinguishes the individual user and retrieves the corresponding data.
Creating the PostgREST Application
Using the Command Line
- On your workstation, create an empty Git repository:
mkdir my-postgrest cd my-postgrest git init - Create the application on Scalingo:
scalingo create my-postgrest - Provision a Scalingo for PostgreSQL® Starter 512 add-on:
scalingo --app my-postgrest addons-add postgresql postgresql-starter-512 - Instruct the platform to use a specific buildpack to deploy PostgREST:
scalingo --app my-postgrest env-set BUILDPACK_URL=https://raw.githubusercontent.com/Scalingo/scalingo-labs/main/postgrest-buildpack/postgrest-buildpack.tar.gz - Specify the version of PostgREST you want to deploy:
scalingo --app my-postgrest env-set POSTGREST_VERSION=<version> - Set up the PostgreSQL® connection:
scalingo --app my-postgrest env-set \ PGRST_DB_URI=\$SCALINGO_POSTGRESQL_URL \ PGRST_DB_SCHEMAS=api \ PGRST_SERVER_PORT=\$PORT
Setting Up the Database
- Open a PostgreSQL® console:
scalingo --app my-postgrest pg-console - From the PostgreSQL® console, create a schema that PostgREST will expose and another one that will stay private for internal helpers:
CREATE SCHEMA api; CREATE SCHEMA private; - Create a helper that reads the user ID from the JWT claims:
CREATE FUNCTION private.jwt_sub() RETURNS text LANGUAGE sql STABLE AS $$ SELECT current_setting('request.jwt.claims', true)::jsonb ->> 'sub'; $$; - Create the
todostable in theapischema:CREATE TABLE api.todos ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, owner_id text NOT NULL DEFAULT private.jwt_sub(), task text NOT NULL, done boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now() );The
owner_idcolumn stores the JWTsubclaim. Clients do not need to send this value when they create a todo item. - Enable and force RLS on the
todostable:ALTER TABLE api.todos ENABLE ROW LEVEL SECURITY; ALTER TABLE api.todos FORCE ROW LEVEL SECURITY;FORCE ROW LEVEL SECURITYis important in this tutorial because the database role used by PostgREST also owns the table. A table owner can otherwise bypass RLS. - Create a dedicated policy for each CRUD operation:
CREATE POLICY todos_select_own ON api.todos FOR SELECT USING (owner_id = private.jwt_sub());CREATE POLICY todos_insert_own ON api.todos FOR INSERT WITH CHECK (owner_id = private.jwt_sub());CREATE POLICY todos_update_own ON api.todos FOR UPDATE USING (owner_id = private.jwt_sub()) WITH CHECK (owner_id = private.jwt_sub());CREATE POLICY todos_delete_own ON api.todos FOR DELETE USING (owner_id = private.jwt_sub());
Setting Up Keycloak
- Connect to the admin console of your Keycloak instance
- Create a new realm named
postgrest-demo - In the newly-created realm, create an OpenID Connect client with the following configuration:
- Client ID:
postgrest-api - Client authentication: On
- Standard flow: On
- Direct access grants: On
- Client ID:
- Open Credentials
- Copy the client secret
- Store it locally:
export KEYCLOAK_CLIENT_SECRET="PASTE_THE_SECRET"By creating the realm and the client, we allow the application to authenticate users and validate tokens issued by Keycloak using the client credentials.
-
Add the PostgreSQL® role to the JWT:
- Open the dedicated client scope for
postgrest-api - Select Add mapper → By configuration → Hardcoded claim
- Configure the mapper with:
- Name:
postgrest-role - Token Claim Name:
role - Claim value: your
POSTGREST_DB_USER - Claim JSON Type:
String - Add to access token: On
- Add to ID token: Off
- Add to userinfo: Off
This configuration adds a fixed
roleclaim to every access token issued for the client, allowing PostgREST to connect to PostgreSQL® using the Scalingo database role while relying on the user’s uniquesubclaim to enforce RLS. - Name:
- Open the dedicated client scope for
-
Add an audience to the access token:
- In the same dedicated client scope, select Add mapper → By configuration → Audience
- Configure the mapper with:
- Name:
postgrest-audience - Included Client Audience:
postgrest-api - Add to access token: On
- Name:
- Verify that the access token contains:
{ "aud": "postgrest-api" }The audience ensures that PostgREST only accepts tokens issued for the
postgrest-apiclient.
-
Create a user named
alice:- Create a new user named
alice - Complete the required profile fields
- Configure the user with:
- Email verified: On
- Required user actions: leave empty
- Set a password with:
- Temporary: Off
This ensures the user can request tokens directly with
curlwithout requiring browser interaction or additional setup steps.
- Create a new user named
- Store Alice’s password locally:
export ALICE_PASSWORD="ALICE_TEST_PASSWORD" - Set the public URL of the Keycloak endpoint that exposes the realm:
export KEYCLOAK_URL="https://my-postgrest.<region>.scalingo.io" export KEYCLOAK_REALM="postgrest-demo" - Retrieve the JSON Web Key Set published by Keycloak:
KEYCLOAK_JWKS="$( curl --fail --silent --show-error \ "$KEYCLOAK_URL/realms/$KEYCLOAK_REALM/protocol/openid-connect/certs" \ | jq --compact-output . )" - Configure PostgREST to verify Keycloak JWT signatures and validate the API audience:
scalingo --app "$POSTGREST_APP" env-set \ PGRST_JWT_SECRET="$KEYCLOAK_JWKS" \ PGRST_JWT_AUD="postgrest-api"
Finalizing the PostgREST Deployment
Everything required by PostgREST is now configured.
- Deploy the API:
git commit --allow-empty --message "Deploy PostgREST" git push scalingo main
Testing
- Set the public URL of the deployed application:
export POSTGREST_URL="https://<postgrest-public-domain>" - In your terminal/console/shell, define a helper function that retrieves an access token from Keycloak:
get_token() { local username="$1" local password="$2" curl --fail --silent --show-error --request POST \ "$KEYCLOAK_URL/realms/$KEYCLOAK_REALM/protocol/openid-connect/token" \ --header "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=password" \ --data-urlencode "client_id=postgrest-api" \ --data-urlencode "client_secret=$KEYCLOAK_CLIENT_SECRET" \ --data-urlencode "username=$username" \ --data-urlencode "password=$password" \ | jq --raw-output '.access_token' } - Use the helper to retrieve an access token for user Alice:
export ALICE_TOKEN="$(get_token alice "$ALICE_PASSWORD")" echo "$ALICE_TOKEN" - Verify that Alice’s token contains claims similar to:
{ "sub": "e393ad9b-598e-45a7-b853-a1e888ca680f", "preferred_username": "alice", "role": "$POSTGREST_DB_USER", "aud": "postgrest-api" }The exact
subvalue is generated by Keycloak and differs for every user. - Create a todo using Alice’s access token. Alice does not need to provide
owner_idexplicitly:curl --fail --silent --show-error --request POST \ "$POSTGREST_URL/todos" \ --header "Authorization: Bearer $ALICE_TOKEN" \ --header "Content-Type: application/json" \ --header "Prefer: return=representation" \ --data '{"task":"Deploy PostgREST on Scalingo"}' \ | jqPostgreSQL® automatically fills
owner_idfrom Alice’s verified JWTsubclaim. - List the todos using the same token:
curl --fail --silent --show-error \ "$POSTGREST_URL/todos?select=id,owner_id,task,done" \ --header "Authorization: Bearer $ALICE_TOKEN" \ | jqAlice should only receive her own rows. The HTTP endpoint is the same for every user, but PostgreSQL® returns different data thanks to the RLS policy that compares each row’s
owner_idwith the authenticated user’s verified JWTsubclaim.
You now have a PostgREST API running directly on Scalingo, by using PostgreSQL® and Keycloak and by using Row-Level Security and JWT to isolate each user’s data.