Deploy PostgREST on Clever-Cloud

Introduction

What is PostgREST

https://postgrest.org/en/stable/

PostgREST is a standalone web server that turns your PostgreSQL database directly into a RESTful API. The structural constraints and permissions in the database determine the API endpoints and operations.

In front of a PostgreSQL database, PostgREST is a tool that can generate APIs directly from the database schema, without any code, just a configuration.

With SQL skills, we are able to generate our backend platform with this tool. Of course, this tool is not a silver bullet. For a standard API that access data in a relational database, it is perfect. With functions, native support of JWT authentication, we are able to build a production grade backend.

Clever-Cloud : the French PaaS

https://www.clever-cloud.com/

Clever Cloud is a Platform as a Service, aiming at helping companies to ship way faster their web services in the cloud, with a pay-as-you-go pricing model. No infrastructure to manage, no system to update and no orchestrator to troubleshoot.

In other words : “git push” your app, and you’re done.

Why using PostgREST

A SGBD (in our case PostgreSQL) has many features in addition to storage :

  • multi users support
  • views
  • any kind of control on insert with triggers
  • functions
  • pagination

Many of those features are not used by standard ORMs. Some controls or filters are not done directly on the database but after. Of course, ORMs supports native functionnalities but we know projects that uses ORMs just as a layer between data and API, all controls are done in the application server instead of using database native features.

For many use cases, we need to access data from a database through an API. This is exactly what PostgREST does without any difficulties, just rely on database native features.

For us, PostgREST provides :

  • fast deployment
  • new APIs on the fly just by creating views
  • performance and scalability
  • less code to maintain

If PostgREST is used safely (see below), it has all needed waranties for accessing a database through a REST API.

The right way (for us) to use Postgrest safely

At please-open.it, we use PostgREST in several projects that are in production without any issue. We have now a set of recommendations and advices to get something :

  • safe
  • upgradable
  • easy to maintain

The PostgREST documentation https://postgrest.org/en/stable/references/api/tables_views.html does not give any advice about architecture, only the tool capabilities for exposing tables, views and function as REST endpoints.

History : databases in the 70’s, and 80’s

https://en.wikipedia.org/wiki/Database

https://www.cockroachlabs.com/blog/history-of-databases-distributed-sql/

Managing a large set of data in the 80’s meant giving access to that data for users. A database administrator was in charge of this part, the database management system already supported multi users. Each user can request data with a terminal :

Alt text

On each table, access is granted through the “GRANT” command (and still exists).

Power of the VIEW

https://en.wikipedia.org/wiki/View_(SQL)

a view is the result set of a stored query, which can be queried in the same manner as a persistent database collection object.

With modern architectures we do not create views (and sometimes we never write SQL queries, we trust in ORMs), but a view have 2 main features that could change the game :

  • preset complex queries with a direct access just by a “SELECT”
  • grant access to a user to this view

Remember the MVC pattern ?

  • Model : our data model
  • View …
  • Controller : triggers in SQL

Our views will be the exposed APIs.

Our architecture and use of PostgREST

Separate uses with Schemas

A view is an API, it should be “usable” out of the box. If we have a relational model and we expose tables directly, we have to make JOIN, FILTER, ORDER BY in the request. This is not how we do it.

Alt text

We build our API only for our needs, by using specific views.

If we connect directly PostgREST to the “public” schema, we have to make the request so it will look like this :

GET /films?select=id,directors(first_name,last_name),title,year,rating,language

With view :

GET /films

With our model, by using a view, the API documentation is enough. If we plug directly to the tables, any API user needs to know the data model in order to make requests.

Controllers with triggers

While inserting data with any kind of API, we have to control before insertion. Data types are not enough, for specific needs we use a native feature : triggers.

https://www.postgresql.org/docs/current/sql-createtrigger.html

Insertions and updates

https://www.postgresql.org/docs/15/sql-createview.html

PostgreSQL allow INSERT, UPDATE and DELETE on “simple views” (“Updatable Views” in the documentation).

According to the documentation :

  • The view must have exactly one entry in its FROM list, which must be a table or another updatable view.
  • The view definition must not contain WITH, DISTINCT, GROUP BY, HAVING, LIMIT, or OFFSET clauses at the top level.
  • The view definition must not contain set operations (UNION, INTERSECT or EXCEPT) at the top level.
  • The view’s select list must not contain any aggregates, window functions or set-returning functions.

With all those 4 points, we know how to create “updatable views”, so as described in the PostgREST documentation we can insert, update or delete data through our views.

https://postgrest.org/en/stable/references/api/tables_views.html#insert

Deploy from sources

From Github repo

Postgrest is available on Github : https://github.com/PostgREST/postgrest

The repository does not have a branch per release but only a tag : https://github.com/PostgREST/postgrest/tags.

Clever-cloud can not deploy from a tag, only from branches. The easiest solution is :

  • fork the postgrest project on your own github account
  • create a branch from the tag
  • push it to github
  • deploy on Clever-cloud

First of all, clone the repository. Do not forget to uncheck “copy the main branch only”.

fork.png

Now, you can clone it on your machine, then create the branch from the tag you want :

git checkout -b 11.0.1 tags/v11.1.0

Then :

git push --set-upstream origin 11.0.1

Deploy on Clever-cloud

Link your Github account to your Clever-cloud account. After that, you will have access to all your github repositories.

Go to “create” then “an application”. Select “postgrest” in the list :

create-app.png

Select “Haskell” application type :

Then in the “scalability” section, edit the default param and select a “pico” instance which is widely enough for most use cases :

Select your zone, and now click on “Create”. The application will be built and launched. It results in a failure because we have to set it up.

In the “informations” tab, you can select the branch you want to deploy. In our case 11.1.0 :

Create and prepare a database

Register a new database

https://www.clever-cloud.com/doc/deploy/addon/postgresql/postgresql/

Postgrest relies on a PostgreSQL database. Clever-Cloud has a PostgreSQL database as a service we will use.

From your administration console, create a new add-on then select “postgres”. For our use, we will not select a “DEV” instance.

Link it to your Postgrest application created previously :

A schema for data, another for APIs

In your add-on dashboard, open PG Studio :

Note : you can use any Postgresql client as you like.

As explained previously, we use a specific schema for APIs. You should not expose your model that contains your data. Retrieve the user of your database in the add-on dashboard.

CREATE SCHEMA api;

GRANT USAGE ON SCHEMA api to <you_user>;
-- GRANT ALL ON SCHEMA api to <you_user>;
-- We can give all access to our API, but for this example we will give access view by view.

Set up PostgREST

PostgREST can be set up with a configuration file or directly with environment variables (like Keycloak) :

We need at least :

  • PGRST_DB_ANON_ROLE : which role to use as anonymous role. In our case, it is the username
  • PGRST_DB_SCHEMAS : schema to expose
  • PGRST_DB_URI : connection to the database. Just copy/paste POSTGRESQL_ADDON_URI value
  • PGRST_SERVER_PORT : by default Clever-Cloud expects an application to listen on port 8080.
  • PGREST_OPENAPI_SERVER_PROXY_URI : our domain name

Alt text

Restart the application. From the “domain names” tab, open your application and now you have a JSON File for OpenAPI.

You’re done !

How we recommend using postgREST

With views

In our case, we directly connect Postgrest to a Keycloak database. All tables are in a schema called “public”, to link an API to a table or a set of tables, we create some views :

CREATE VIEW api.client AS (select id, client_id from public.client);
GRANT SELECT ON api.client to <user>;

Our API is now created, check in the OpenAPI schema :

Alt text

Only get on /client. Try this API and you will have only id and client_id returned.

Of course, a view can contains filters :

DROP VIEW api.client;
CREATE VIEW api.client AS (select id, client_id from public.client where public.client.enabled=false);
GRANT SELECT ON api.client to <user>;

The API will return only disabled clients.

Add an authentication layer (because we are please-open.it)

Postgrest has some capabilities for a multi users application, with 2 differents ways :

  • using local accounts, with a “users” table
  • JWT token, in our case we will generate them with Keycloak

Using local accounts

Postgrest has the ability to manage users with a table and a set of functions. A “login” API is created with a function, this function is designed to return a JWT token.

After that, every request has to contain a JWT token for authentication, PostgREST does not have a session management system.

https://postgrest.org/en/stable/how-tos/sql-user-management.html#sql-user-management

Because it uses a non official extension (pgjwt), we can not apply this method to our deployment on Clever-Cloud.

Use this documentation as an example to see how it is possible to execute custom functions from an API Call. Of course, we can make our own session management with a special table that contains all current sessions. But, this is a bad design for the DB performance.

Using Keycloak

The public key, for token signature verification

JWT tokens are signed, by default, with the RSA256 algorithm. We need to retrieve the public key in order to bring it to postgres for signature verification. In the “realm settings” section, go to “Keys” tab and grab the public key :

Alt text

This is a PEM format, PostgREST needs a JWK so we need a converter. Many exists, get the simplest : https://irrte.ch/jwt-js-decode/pem2jwk.html

We give the entire result to PostgREST in the variable PGRST_JWT_SECRET.

Filtering

Native roles (unavailable on Clever-cloud)

Due to limitations on PostgreSQL databases on Clever-Cloud, we can not use native roles.

With the parameter PGRST_JWT_ROLE_CLAIM_KEY we inform PostgREST where to find user roles in the JWT token. A mapping between PostgreSQL roles and given roles in the token is done.

We have to grant access to each view for concerned roles. So a user with needed roles in the token could request the view.

https://postgrest.org/en/stable/references/auth.html?highlight=role#overview-of-role-system

Check on variables

When you call an API with a JWT Token, all token informations are available as environment variables in the request.

Because we have views, we make our own SELECT request, we can make a kind of “row level filtering” https://www.postgresql.org/docs/current/ddl-rowsecurity.html :

CREATE VIEW api.me AS (
    SELECT id, username FROM public.user
    WHERE id = current_setting('request.jwt.sub', true)
);

Row Level Security policies are not available on views in PostgreSQL. It does not matter, we can write down our policy directly in the SQL request.

Other cool features

Pre-request

https://postgrest.org/en/stable/references/transactions.html#pre-request

If you need to check specific access (header, cookie …), a SQL function can be called before each request. With all variables set by PostgREST, a control can be done every time.

Set response headers

https://postgrest.org/en/stable/references/transactions.html?highlight=response.headers#response-headers

With the configuration “response.headers”, some advanced features about images could be done with functions for example :

https://postgrest.org/en/stable/how-tos/providing-images-for-img.html?highlight=response.headers#improved-version

Responses types

https://postgrest.org/en/stable/references/api/resource_representation.html?highlight=csv#response-format

Want a CSV instead of a JSON structure ? Just add Accept: text/csv header and that’s it !

Bulk insert

https://postgrest.org/en/stable/references/api/tables_views.html?#bulk-insert

One request to create several elements ? Bulk insert is supported if you give a CSV list or an array in JSON.

Comments for OpenAPI documentation

https://postgrest.org/en/stable/references/api/openapi.html#openapi

COMMENT ON VIEW api.me IS
  'Returns the current user';

COMMENT ON COLUMN api.me.id IS
  'The userid is the subject field in JWT token';

Conclusion

The postgREST documentation explains how to technically use the tool, with tables, procedures, views … And a “getting started” documentation shows out of the box all capabilities of this great tool.

Using postgREST directly out of the box connected to tables is not great at many points :

  • data security, you have to think about all rules on each table
  • exposes data model
  • needs to know data model for join
  • no clear separation of what is exposed or not

Because this tool can use all the power of PostgreSQL, with some principles (separate schemas, views, filter direct in SQL requests), it shows all the power with less than a half of … 256Mb of RAM.

Alt text