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.