Database Setup

Nova does not include a built-in database layer — by design, you choose what fits your project. We will use Kura, an Ecto-inspired database abstraction for Erlang that targets PostgreSQL. Kura gives you schemas, changesets, a query builder, and migrations — no raw SQL required.

Adding dependencies

Add kura and the rebar3_kura plugin to rebar.config:

{deps, [
        nova,
        {flatlog, "0.1.2"},
        {kura, "~> 1.0"}
       ]}.

{plugins, [
    rebar3_nova,
    {rebar3_kura, "~> 1.0"}
]}.

Also add kura to your application dependencies in src/blog.app.src:

{applications,
 [kernel,
  stdlib,
  nova,
  kura
 ]},

Setting up the repository

The rebar3_kura plugin provides a setup command that generates a repository module:

rebar3 kura setup --name blog_repo

This creates src/blog_repo.erl — a module that wraps all database operations:

-module(blog_repo).
-behaviour(kura_repo).

-export([config/0, start/0, all/1, get/2, get_by/2, one/1,
         insert/1, insert/2, update/1, delete/1,
         update_all/2, delete_all/1, insert_all/2,
         preload/3, transaction/1, multi/1, query/2]).

config() ->
    #{pool => blog_repo,
      database => <<"blog_dev">>,
      hostname => <<"localhost">>,
      port => 5432,
      username => <<"postgres">>,
      password => <<>>,
      pool_size => 10}.

start() -> kura_repo_worker:start(?MODULE).
all(Q) -> kura_repo_worker:all(?MODULE, Q).
get(Schema, Id) -> kura_repo_worker:get(?MODULE, Schema, Id).
get_by(Schema, Clauses) -> kura_repo_worker:get_by(?MODULE, Schema, Clauses).
one(Q) -> kura_repo_worker:one(?MODULE, Q).
insert(CS) -> kura_repo_worker:insert(?MODULE, CS).
insert(CS, Opts) -> kura_repo_worker:insert(?MODULE, CS, Opts).
update(CS) -> kura_repo_worker:update(?MODULE, CS).
delete(CS) -> kura_repo_worker:delete(?MODULE, CS).
update_all(Q, Updates) -> kura_repo_worker:update_all(?MODULE, Q, Updates).
delete_all(Q) -> kura_repo_worker:delete_all(?MODULE, Q).
insert_all(Schema, Entries) -> kura_repo_worker:insert_all(?MODULE, Schema, Entries).
preload(Schema, Records, Assocs) -> kura_repo_worker:preload(?MODULE, Schema, Records, Assocs).
transaction(Fun) -> kura_repo_worker:transaction(?MODULE, Fun).
multi(Multi) -> kura_repo_worker:multi(?MODULE, Multi).
query(SQL, Params) -> kura_repo_worker:query(?MODULE, SQL, Params).

Every function delegates to kura_repo_worker with the repo module as the first argument. The config/0 callback tells Kura how to connect to PostgreSQL.

The setup command also creates src/migrations/ for migration files.

PostgreSQL with Docker Compose

Create docker-compose.yml in your project root:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: blog_dev
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Start it:

docker compose up -d

Configuring the repo

Update config/dev_sys.config.src to include the repo config. The repo module reads its config from config/0, but you can also configure it through sys.config if you prefer environment variable substitution:

[
  {kernel, [
    {logger_level, debug},
    {logger, [
      {handler, default, logger_std_h,
        #{formatter => {flatlog, #{
            map_depth => 3,
            term_depth => 50,
            colored => true,
            template => [colored_start, "[\033[1m", level, "\033[0m",
                         colored_start, "] ", msg, "\n", colored_end]
          }}}}
    ]}
  ]},
  {nova, [
         {use_stacktrace, true},
         {environment, dev},
         {cowboy_configuration, #{port => 8080}},
         {dev_mode, true},
         {bootstrap_application, blog},
         {plugins, [
                    {pre_request, nova_request_plugin, #{
                        read_urlencoded_body => true,
                        decode_json_body => true
                    }}
                   ]}
        ]}
].

Starting the repo in the supervisor

The repo needs to be started when your application boots. Add it to your supervisor in src/blog_sup.erl:

-module(blog_sup).
-behaviour(supervisor).

-export([start_link/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
    blog_repo:start(),
    {ok, {#{strategy => one_for_one, intensity => 5, period => 10}, []}}.

blog_repo:start() creates the pgo connection pool using the config from config/0.

Adding the rebar3_kura compile hook

To get automatic migration generation (covered in the next chapter), add a provider hook to rebar.config:

{provider_hooks, [
    {post, [{compile, {kura, compile}}]}
]}.

This runs rebar3 kura compile after every rebar3 compile, scanning your schemas and generating migrations for any changes.

Verifying the connection

Start the development server:

rebar3 nova serve

You should see the application start without errors. If the database is unreachable, you will see a connection error in the logs. Verify from the shell:

1> blog_repo:query("SELECT 1", []).
{ok, #{command => select, num_rows => 1, rows => [{1}]}}

Two commands and you have a database layer.


Now let's define our first schemas and watch Kura generate migrations automatically in Schemas and Migrations.