Skip to main content

Step 1 — Set up the CRM project

This tutorial walks you through building a working Customer Relationship Management app on the Liberty Framework — six steps, from an empty install to a deployed application with screens, a dashboard, OIDC sign-in, an AI assistant and a scheduled job.

By the end of this step you'll have the framework running, an empty crm app registered and the three database tables we'll use throughout. Estimated time: 10 minutes.


What we're building

Across the six steps:

StepWe addThe framework concept it teaches
1 — Setup (this page)Empty app + three database tables.App / Pool — what data we're going to reach.
2 — CustomersFirst screen — list of customers with edit dialog.Connector / Screen — the framework's primary surface.
3 — DealsSecond screen with FK to customers + an Activities sub-grid.Lookups / Relationships — how entities reference each other.
4 — DashboardSales-pipeline dashboard with KPIs + chart + drill-down.Dashboard / Charts — summarise the data.
5 — AuthTwo roles + OIDC sign-in.Authentication / Roles — who sees what.
6 — AI + JobsAI assistant over the data + nightly summary job.AI / Nomaflow — natural-language access + scheduled work.

The CRM domain is generic on purpose — three entities, recognisable relationships, no vendor specifics. Once the pattern clicks, the same approach applies to whatever your real domain is.


Prerequisites

You needWhy
Liberty Framework installed locallyFollow Getting Started → Installation up to "Verify the install". The framework should be reachable at http://127.0.0.1:8000 with the admin user signed in.
PostgreSQL or the built-in SQLiteThe tutorial uses generic SQL; either works. SQLite is faster to start (already wired).
A SQL clientpsql for PostgreSQL or sqlite3 for SQLite. Anything that lets you run a CREATE TABLE.

If you haven't installed the framework yet, do that first and come back here.


Create the database tables

Connect to your database (sqlite3 liberty.db for SQLite, psql for PostgreSQL) and run:

CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
industry VARCHAR(64),
country VARCHAR(2),
status VARCHAR(32) NOT NULL DEFAULT 'active',
primary_email VARCHAR(255),
created_by VARCHAR(64),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_by VARCHAR(64),
updated_at TIMESTAMP
);

CREATE TABLE deals (
id SERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
name VARCHAR(255) NOT NULL,
stage VARCHAR(32) NOT NULL DEFAULT 'qualified',
amount DECIMAL(12,2) NOT NULL DEFAULT 0,
currency VARCHAR(3) NOT NULL DEFAULT 'EUR',
close_date DATE,
owner VARCHAR(64),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE activities (
id SERIAL PRIMARY KEY,
deal_id INTEGER NOT NULL REFERENCES deals(id),
kind VARCHAR(32) NOT NULL,
notes TEXT,
happened_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
recorded_by VARCHAR(64)
);

-- A small reference table for the deal stages
CREATE TABLE deal_stages (
code VARCHAR(32) PRIMARY KEY,
label VARCHAR(64) NOT NULL,
colour VARCHAR(16),
ord INT NOT NULL DEFAULT 0
);

INSERT INTO deal_stages(code, label, colour, ord) VALUES
('qualified', 'Qualified', '#60a5fa', 10),
('proposal', 'Proposal', '#c084fc', 20),
('negotiation', 'Negotiation', '#fb923c', 30),
('won', 'Won', '#4ade80', 40),
('lost', 'Lost', '#f87171', 50);

-- A couple of customers to play with
INSERT INTO customers(name, industry, country, primary_email) VALUES
('Acme Industries SA', 'manufacturing', 'FR', 'contact@acme.example'),
('Globex Logistics', 'logistics', 'DE', 'sales@globex.example'),
('Initech Services', 'services', 'FR', 'hello@initech.example');

INSERT INTO deals(customer_id, name, stage, amount, currency, close_date) VALUES
(1, 'Annual contract renewal', 'proposal', 45000.00, 'EUR', '2026-07-15'),
(1, 'Add-on training package', 'qualified', 8500.00, 'EUR', '2026-08-30'),
(2, 'Logistics platform Q3', 'negotiation', 120000.00, 'EUR', '2026-09-10'),
(3, 'Support contract', 'won', 18000.00, 'EUR', '2026-06-01');

If you're on SQLite, replace SERIAL PRIMARY KEY with INTEGER PRIMARY KEY AUTOINCREMENT and TIMESTAMP DEFAULT CURRENT_TIMESTAMP with DATETIME DEFAULT CURRENT_TIMESTAMP. The rest works as-is.

Verify with one query:

SELECT count(*) FROM customers; -- → 3

Register the crm app

Open the framework in your browser, sign in as admin, click the gear icon (top right) to open Settings. Switch to the Apps tab and click + New app.

Settings → Apps → + New app
ID
crm
Display name
CRM
Description
Customer relationship management — tutorial app
Icon
users ▾
Order
10
CancelSave

Click Save. The framework records the app metadata. Nothing visible changes yet — apps only show up in the workspace selector once they have a menu (we'll add one in Step 2).

What just happened

You declared a namespace. From here on, every connector, screen, menu, dashboard and job we create will carry app = "crm" so the framework groups them under this workspace.

You don't normally have to declare the app upfront — it would be created on first reference. We did it now so the Display name, Icon and Order are set; otherwise the workspace selector would show the raw identifier.


Use the default pool

We won't create a new pool — the default pool that ships with the framework points at your local SQLite (or whatever you configured in app.toml). For a one-host tutorial that's enough.

To confirm, open Settings → Pools. You should see one row, default, with the connected status.

If you ran the SQL above against a different database, head to Settings → Pools → + New pool and add it now — the rest of the tutorial assumes a pool the framework can reach. The pool name will be referenced in Step 2 when we build the connector.


Verify

CheckHow
Framework reachablehttp://127.0.0.1:8000 returns the home page.
Admin signed inTop right shows the admin chip.
Database tables existSELECT count(*) FROM customers; returns 3.
crm app registeredSettings → Apps lists crm.
Pool connectedSettings → Pools shows default (or your custom pool) as connected.

What you have now

Nothing visible yet — but the foundation is in place: a registered app, a reachable pool, three tables with seed data. The next step turns this into an actual screen.

Step 2 — Customers — define your first connector, build the Customers screen, add it to the menu.