<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Postgrest on kistu</title>
  <link rel="alternate" href="/tags/postgrest/" />
  <link rel="self" href="/tags/postgrest/index.xml" />
  <subtitle>Recent content in Postgrest on kistu</subtitle>
  <id>/tags/postgrest/</id>
  <generator uri="http://gohugo.io" version="0.124.0">Hugo</generator>
  <language>en</language>
  <updated>2026-02-16T19:41:13+02:00</updated>
  <author>
    
    
  </author>
  
      <entry>
        <title>PostgreSQL with a REST API</title>
        <link rel="alternate" href="/posts/003-postgresql-with-a-rest-api/" />
        <id>/posts/003-postgresql-with-a-rest-api/</id>
        <published>2026-02-16T19:41:13+02:00</published>
        <updated>2026-04-21T12:16:04+02:00</updated>
        <summary type="html">Context The requirement is to implement a new database design for managing a variety of digital assets. Each asset has an associated identifier (hash) and metadata. The hash is calculated using a tool like sha512sum or phash. Metadata is extracted in JSON format using tools like ffprobe or exiftool. Ideally, only specific required metadata fields will be stored in the database, but that makes future changes more complex and requires making certain assumptions about the type of digital asset.</summary>
          <content type="html"><![CDATA[<h1 id="context">Context</h1>
<p>The requirement is to implement a new database design for managing a variety of digital assets. Each asset has an associated identifier (hash) and metadata. The hash is calculated using a tool like <code>sha512sum</code> or <code>phash</code>. Metadata is extracted in JSON format using tools like <code>ffprobe</code> or <code>exiftool</code>. Ideally, only specific required metadata fields will be stored in the database, but that makes future changes more complex and requires making certain assumptions about the type of digital asset. To make the database as general purpose as possible, the database management system (DBMS) needs to support JSON documents.</p>
<h1 id="postgresql">PostgreSQL</h1>
<p><a href="https://www.postgresql.org">PostgreSQL</a> is an open source relational database that supports JSON types. To better understand the configuration of this database, it is beneficial to briefly cover some terminology and hierarchies.</p>
<p>PostgreSQL uses a client-server model. The server accepts client connections at a specified port (5432 by default) and stores data at a specified location (typically <code>/usr/local/pgsql/data</code> or <code>/var/lib/postgresql/data</code>). The server can manage any number of databases stored in this data directory; this forms a single database cluster. The database itself consists of schemas; schemas act as a namespace for organizing database objects like tables and functions (SQL queries).</p>
<p>



    
    <input type="checkbox" id="zoomCheck-bb973" hidden>
    <label for="zoomCheck-bb973">
        <img class="zoomCheck" loading="lazy" decoding="async" 
            src="postgresql-organization-hierarchy.svg" alt="postgresql organization hierarchy" 
             />
    </label>

</p>
<p>PostgreSQL uses roles and privileges for access control. Roles are created and assigned permissions to perform specific actions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">create role role_name password <span class="s1">&#39;role_password&#39;</span><span class="p">;</span> 
</span></span><span class="line"><span class="cl">grant usage on schema schema_name to role_name<span class="p">;</span>  <span class="c1"># allows accessing objects under specified schema</span>
</span></span><span class="line"><span class="cl">grant INSERT on schema.table to role_name<span class="p">;</span>       <span class="c1"># allows action on specified object</span>
</span></span></code></pre></div><p>The specified action can be ALL, or INSERT, SELECT, UPDATE, DELETE and so on.</p>
<h1 id="implementation">Implementation</h1>
<p>Docker is used to deploy the services. Three services are used:</p>
<ul>
<li><code>db</code>: the PostgreSQL server and database cluster</li>
<li><a href="https://www.adminer.org/en/"><code>adminer</code></a>: webpage that allows running SQL commands</li>
<li><code>api</code>: <a href="https://docs.postgrest.org">Postg<strong>REST</strong></a> web server that exposes a REST API</li>
</ul>
<p>



    
    <input type="checkbox" id="zoomCheck-779ce" hidden>
    <label for="zoomCheck-779ce">
        <img class="zoomCheck" loading="lazy" decoding="async" 
            src="services-connection.svg" alt="services connection" 
             />
    </label>

</p>
<p><code>psql</code> is a terminal based client interface for the PostgreSQL server. <code>pgAdmin</code> and <code>phpMyAdmin</code> are graphical interfaces similar to <code>adminer</code>; GUIs make running SQL commands easier by eliminating the need for terminal access. Adminer can be used to create schemas, perform CRUD operations on tables, create roles, assign permissions and execute other SQL commands.</p>
<p>



    
    <input type="checkbox" id="zoomCheck-b31bc" hidden>
    <label for="zoomCheck-b31bc">
        <img class="zoomCheck" loading="lazy" decoding="async" 
            src="adminer-interface.png" alt="adminer interface" 
             />
    </label>

</p>
<p>The <code>api</code> is used to enable clients to perform actions without specialized tools for connecting to the database; only the ability to send HTTP requests is required.</p>
<h2 id="docker">Docker</h2>
<p>Create <code>docker-compose.yml</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">postgres:18.2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_USER</span><span class="p">:</span><span class="w"> </span><span class="l">${ADMIN_USERNAME}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${ADMIN_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_DB</span><span class="p">:</span><span class="w"> </span><span class="l">${DATABASE_NAME}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">pgdata:/var/lib/postgresql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">adminer</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">adminer:5.4.2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">8080</span><span class="p">:</span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">api</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">postgrest/postgrest:v14.5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">3000</span><span class="p">:</span><span class="m">3000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PGRST_SERVER_HOST</span><span class="p">:</span><span class="w"> </span><span class="m">0.0.0.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PGRST_DB_URI</span><span class="p">:</span><span class="w"> </span><span class="l">postgres://${AUTH_ROLE_USERNAME}:${AUTH_ROLE_PASSWORD}@db:5432/${DATABASE_NAME}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PGRST_OPENAPI_SERVER_PROXY_URI</span><span class="p">:</span><span class="w"> </span><span class="l">http://127.0.0.1:3000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PGRST_JWT_SECRET</span><span class="p">:</span><span class="w"> </span><span class="l">${JWT_SECRET}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pgdata</span><span class="p">:</span><span class="w">
</span></span></span></code></pre></div><p>Define the variables in <code>.env</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-env" data-lang="env"><span class="line"><span class="cl"><span class="nv">ADMIN_USERNAME</span><span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="nv">ADMIN_PASSWORD</span><span class="o">=</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">DATABASE_NAME</span><span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="nv">DATABASE_SCHEMAS</span><span class="o">=</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">AUTH_ROLE_USERNAME</span><span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="nv">AUTH_ROLE_PASSWORD</span><span class="o">=</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">JWT_SECRET</span><span class="o">=</span>
</span></span></code></pre></div><blockquote>
<p>Use <code>echo $(&lt; /dev/urandom tr -dc A-Za-z0-9 | head -c32)</code> to generate secure alphanumeric passwords; change length (to <code>-c64</code>, <code>c128</code>, &hellip;) as required. The JWT secret must be 32 characters.</p>
</blockquote>
<p>Start the services with <code>docker compose up -d</code>.</p>
<h2 id="database-roles">Database roles</h2>
<p>Two roles are used to separate authentication (verifying identity) from authorization (verifying permissions). Clients connect to the <code>authenticator</code> role by providing the correct password, but this role does not have any permissions. The client can then switch to another role (<code>SET ROLE role_name</code>) which has the required permissions.</p>
<p>The following can be executed using a client interface (like <code>psql</code> or <code>Adminer</code>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">create role role_name nologin<span class="p">;</span> 
</span></span><span class="line"><span class="cl">create role authenticator noinherit login password <span class="s1">&#39;example_authenticator_password&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">grant role_name to authenticator<span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">create schema schema_name<span class="p">;</span>
</span></span><span class="line"><span class="cl">grant usage on schema schema_name to role_name<span class="p">;</span>
</span></span><span class="line"><span class="cl">grant all on schema_name.table_name to role_name<span class="p">;</span>
</span></span></code></pre></div><blockquote>
<p>The nologin rolename and authenticator rolename and password should match that defined in <code>.env</code>.</p>
</blockquote>
<ul>
<li><code>nologin</code> disables connecting directly as the specified role</li>
<li><code>noinherit</code> prevents default permissions from being automatically assigned</li>
</ul>
<p>The containers need to be restarted for <code>postgrest</code> to start without errors:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">docker compose down
</span></span><span class="line"><span class="cl">docker compose up -d
</span></span></code></pre></div><p>Check for keywords <code>error</code> or <code>fatal</code> in the logs (<code>docker compose logs</code>).</p>
<h2 id="api-token-generation">API token generation</h2>
<p>The following steps are from the PostgREST documentation <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>Use the JWT secret defined in <code>.env</code>. Create a signed token using bash and openssl, replacing role_name with the nologin role:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="nb">set</span> -e
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">JWT_SECRET</span><span class="o">=</span><span class="s1">&#39;test_secret_that_is_at_least_32_characters_long&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">_base64 <span class="o">()</span> <span class="o">{</span> openssl base64 -e -A <span class="p">|</span> tr <span class="s1">&#39;+/&#39;</span> <span class="s1">&#39;-_&#39;</span> <span class="p">|</span> tr -d <span class="s1">&#39;=&#39;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">header</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> -n <span class="s1">&#39;{&#34;alg&#34;:&#34;HS256&#34;,&#34;typ&#34;:&#34;JWT&#34;}&#39;</span> <span class="p">|</span> _base64<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">payload</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> -n <span class="s2">&#34;{\&#34;role\&#34;:\&#34;role_name\&#34;}&#34;</span> <span class="p">|</span> _base64<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">signature</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> -n <span class="s2">&#34;</span><span class="nv">$header</span><span class="s2">.</span><span class="nv">$payload</span><span class="s2">&#34;</span> <span class="p">|</span> openssl dgst -sha256 -hmac <span class="s2">&#34;</span><span class="nv">$JWT_SECRET</span><span class="s2">&#34;</span> -binary <span class="p">|</span> _base64<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -n <span class="s2">&#34;</span><span class="nv">$header</span><span class="s2">.</span><span class="nv">$payload</span><span class="s2">.</span><span class="nv">$signature</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>Run this script (<code>./script_name.sh</code>) to create the token.</p>
<h2 id="api-usage">API usage</h2>
<p>Set your token value (this needs to be repeated when a new shell is opened):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">read</span> TOKEN
</span></span></code></pre></div><p>To retrieve records:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl https://domain_name.org/table_name -X GET <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -H <span class="s2">&#34;Authorization: Bearer </span><span class="nv">$TOKEN</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -H <span class="s2">&#34;Accept-Profile: schema_name&#34;</span>
</span></span></code></pre></div><blockquote>
<p>Note, the schema must be created manually (using Adminer) and defined in the environment (<code>DATABASE_SCHEMAS</code>) with the containers restarted after updates for the schema to be accessible. <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p>
</blockquote>
<p>To insert new data:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl https://domain_name.org/table_name -X POST <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -H <span class="s2">&#34;Authorization: Bearer </span><span class="nv">$TOKEN</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -d <span class="s2">&#34;{&#34;</span>column1<span class="s2">&#34;: &#34;</span>value<span class="s2">&#34;, &#34;</span>column2<span class="s2">&#34;: {key1: value1, key2: value2}, &#34;</span>column3<span class="s2">&#34;: 3 }&#34;</span>
</span></span></code></pre></div><h2 id="troubleshooting">Troubleshooting</h2>
<p>There are two different ways of approaching troubleshooting - by starting with something that doesn&rsquo;t work and removing/modifying components until does, or by starting with something that does work and adding components until it doesn&rsquo;t. The second approach is easier when there are many things that could go wrong.</p>
<p>A common error that may be faced is that the binding port is already used. The syntax for port binding is <code>- host_port:container_port</code>. The host post may need to be changed if there is a conflict.</p>
<p>When modifying the <code>docker-compose.yml</code>, if you mount the <code>pgdata</code> volume to <code>/var/lib/postgresql/data</code> and try to access the API, it will state that the <code>JWT secret is not set</code>. If you have a look at logs of the PostgreSQL service, you will notice that it is actually not running - the error references a pull request which requires that the volume be mounted at <code>/var/lib/postgresql</code> instead for PostgreSQL v18+ <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>. Determining this true cause by troubleshooting the API would have been tedious and time consuming.</p>
<p>The PostgREST REST API requires cache invalidation; if faced with errors of the format <code>Could not find xxx in the schema cache</code> and after verification that xxx indeed exists in the specified schema, simply stop and start the service to update cache.</p>
<p>Note, one PostgREST instance can only connect to one database instance. If API access is required for multiple databases in the same database cluster, each database requires a PostgREST instance <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>. Depending on your requirements, using multiple schemas may be a better option.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://docs.postgrest.org/en/v14/tutorials/tut1.html#step-3-sign-a-token">https://docs.postgrest.org/en/v14/tutorials/tut1.html#step-3-sign-a-token</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://docs.postgrest.org/en/v14/references/api/schemas.html">https://docs.postgrest.org/en/v14/references/api/schemas.html</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://github.com/docker-library/postgres/pull/1259">https://github.com/docker-library/postgres/pull/1259</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p><a href="https://github.com/PostgREST/postgrest/issues/1090">https://github.com/PostgREST/postgrest/issues/1090</a>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content>
      </entry>

</feed>


