Installing AGE

backend-engineering

Installing AGE

The relational data model handles most of what this platform needs — users, notes, reading plans, sessions. But Biblical data has a dimension that relational tables express awkwardly: relationships between entities that form dense, navigable networks.

A passage references another passage. A person appears in multiple books. A theological theme connects dozens of unrelated verses. A word in Greek shares its root with a cluster of other words. These relationships are not just attributes — they are the structure. The interesting queries are about traversal: "what passages is this person connected to, two hops out?" or "which themes are shared between these two books?"

Relational joins can express some of this, but recursive CTEs and multi-hop joins get unwieldy fast. A graph database expresses it naturally. Apache AGE (A Graph Extension) brings graph storage and Cypher query language directly into Postgres — same database, same connection, same transactions, but with graph traversal capabilities alongside standard SQL.

This article covers installing AGE, configuring it, and verifying it works. Articles 18 and 19 cover using it.

What Apache AGE Is

AGE is a Postgres extension that adds:

  • Graph storage: vertices and edges stored in Postgres tables, managed by the extension
  • Cypher query language: the graph query language popularized by Neo4j, executed via a SQL wrapper function
  • ACID compliance: graph operations participate in standard Postgres transactions
  • SQL interoperability: graph query results can be joined with regular table data in the same query

It runs inside your existing Postgres instance. No separate graph database process, no separate connection, no data synchronization between systems. The graph data lives in the same database as everything else.

The tradeoff: AGE is a younger, less battle-tested extension than Postgres itself. It works well for the query patterns that fit graph traversal. For very large graphs (hundreds of millions of edges), purpose-built graph databases like Neo4j will outperform it. For the scale of Biblical relationship data on this platform — millions of edges at most — AGE is the right tool.

Prerequisites

Postgres version: AGE requires Postgres 11–16. I run Postgres 16. Check your version:

psql --version
# PostgreSQL 16.2

Build tools (if building from source):

# Ubuntu / Debian
sudo apt-get install -y \
  build-essential \
  libreadline-dev \
  zlib1g-dev \
  flex \
  bison \
  postgresql-server-dev-16

# macOS (with Homebrew)
brew install postgresql@16
# build tools come with Xcode Command Line Tools
xcode-select --install

pg_config must be in PATH and must point to the correct Postgres version:

pg_config --version
# PostgreSQL 16.2

which pg_config
# /usr/lib/postgresql/16/bin/pg_config  (Linux)
# /opt/homebrew/opt/postgresql@16/bin/pg_config  (macOS)

If pg_config points to the wrong version (common when multiple Postgres versions are installed), set it explicitly:

export PATH=/usr/lib/postgresql/16/bin:$PATH

Installation Options

There are three ways to install AGE: from the official package repository (easiest), from source (most control), or via Docker (best for development).

Option 1: Package Repository (Linux)

PGDG (PostgreSQL Global Development Group) distributes AGE packages for recent Postgres versions:

# Add PGDG repository if not already added
sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt-get update

# Install AGE
sudo apt-get install -y postgresql-16-age

Verify the extension files are in place:

ls $(pg_config --sharedir)/extension/age*
# /usr/share/postgresql/16/extension/age--1.5.0.sql
# /usr/share/postgresql/16/extension/age.control

ls $(pg_config --pkglibdir)/age.so
# /usr/lib/postgresql/16/lib/age.so

Option 2: Build from Source

Use this when the package repository does not have your Postgres version or when you need a specific AGE release.

# Clone the AGE repository
git clone https://github.com/apache/age.git
cd age

# Check out the release matching your Postgres version
# See: https://github.com/apache/age/releases
git checkout PG16/v1.5.0-rc0

# Build and install
make
sudo make install

The make step compiles the C extension. It takes 30–60 seconds. Errors at this stage are almost always caused by a missing build dependency or a pg_config version mismatch — check both before troubleshooting further.

After install, verify the extension files are present (same paths as Option 1).

Option 3: Docker (Development)

For local development, the official AGE Docker image is the fastest path:

docker pull apache/age:PG16

docker run \
  --name age-dev \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  -d apache/age:PG16

The image includes Postgres 16 with AGE pre-installed and pre-configured. Connect immediately:

psql -h localhost -U postgres

For a production-like local environment, I use Docker Compose with a persistent volume:

# docker-compose.yml
services:
  postgres:
    image: apache/age:PG16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: bible_study
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d  # initialization scripts

volumes:
  postgres_data:

Configuration

Whether you installed via package or source, two configuration steps are required before AGE works.

1. shared_preload_libraries

AGE must be loaded into the Postgres server process at startup. Edit postgresql.conf:

# Find postgresql.conf
psql -U postgres -c "SHOW config_file;"
# /etc/postgresql/16/main/postgresql.conf

# Edit it
sudo nano /etc/postgresql/16/main/postgresql.conf

Add or update the shared_preload_libraries line:

shared_preload_libraries = 'age'

If other libraries are already listed, append AGE:

shared_preload_libraries = 'pg_stat_statements, age'

Restart Postgres for this change to take effect:

sudo systemctl restart postgresql

Forgetting this step is the most common installation mistake. AGE will appear to load — CREATE EXTENSION age will succeed — but graph queries will fail with errors about missing functions.

2. Create the Extension

Connect to the database where you want graph data and create the extension:

CREATE EXTENSION IF NOT EXISTS age;

This creates the ag_catalog schema, which holds AGE's internal tables and functions. It also installs the Cypher query executor and graph management functions.

Verify:

SELECT * FROM pg_extension WHERE extname = 'age';
--  extname | extowner | extnamespace | extrelocatable | extversion
-- ---------+----------+--------------+----------------+------------
--  age     |       10 |        16408 | f              | 1.5.0

3. search_path Configuration

Every Cypher query in AGE goes through the ag_catalog.cypher() function. To avoid prefixing every query with ag_catalog., add it to the search path for your application role:

-- For a specific role
ALTER ROLE app_user SET search_path = ag_catalog, "$user", public;

-- Or set it per session in your application
SET search_path = ag_catalog, "$user", public;

Without this, every AGE query requires the full ag_catalog.cypher(...) prefix, which is verbose and easy to forget.

For Node.js applications using node-postgres, set it in the pool configuration:

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  // Set search_path on every new connection
  options: "-c search_path=ag_catalog,public",
});

Creating Your First Graph

With AGE installed and configured, create a graph to verify everything works:

-- Load the AGE module for this session (required once per session)
LOAD 'age';
SET search_path = ag_catalog, "$user", public;

-- Create a named graph
SELECT create_graph('bible');

-- Verify the graph was created
SELECT * FROM ag_graph;
--  graphid |  name  | namespace
-- ---------+--------+-----------
--    16409 | bible  | bible

Each named graph gets its own Postgres schema (named after the graph). Vertices and edges are stored in tables within that schema.

Create a vertex and query it:

-- Create a vertex
SELECT * FROM cypher('bible', $$
  CREATE (p:Person { name: 'Paul', role: 'apostle' })
  RETURN p
$$) AS (p agtype);

-- Query it back
SELECT * FROM cypher('bible', $$
  MATCH (p:Person { name: 'Paul' })
  RETURN p.name, p.role
$$) AS (name agtype, role agtype);

--   name  |   role
-- --------+---------
--  "Paul" | "apostle"

If this works, AGE is correctly installed, configured, and operational.

Common Installation Problems

ERROR: could not open extension control file

The extension files are not in the right location for your Postgres installation. Run pg_config --sharedir and confirm age.control exists there. If building from source, make sure pg_config in PATH points to the same Postgres version you are running.

ERROR: LOAD: extension "age" is not in the shared_preload_libraries

shared_preload_libraries = 'age' is not set or Postgres was not restarted after setting it. Restart Postgres and try again.

ERROR: function cypher(unknown, unknown) does not exist

The search_path does not include ag_catalog. Run SET search_path = ag_catalog, "$user", public; in the session, or configure it on the role.

ERROR: permission denied for schema ag_catalog

The application role does not have USAGE on ag_catalog. Grant it:

GRANT USAGE ON SCHEMA ag_catalog TO app_user;
GRANT SELECT ON ALL TABLES IN SCHEMA ag_catalog TO app_user;

Build fails with fatal error: postgres.h: No such file or directory

The Postgres server development headers are not installed. On Ubuntu/Debian: sudo apt-get install postgresql-server-dev-16. Ensure the version number matches your Postgres installation.

Docker Compose for the Full Stack

In development, AGE runs inside the same Docker Compose stack as the rest of the backend. The initialization script creates the extension and sets up the graph automatically:

-- init/01-age.sql (runs automatically on first container start)
CREATE EXTENSION IF NOT EXISTS age;
LOAD 'age';
SET search_path = ag_catalog, "$user", public;
SELECT create_graph('bible');
# docker-compose.yml (relevant section)
services:
  postgres:
    image: apache/age:PG16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: bible_study
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d

With this setup, docker compose up gives a fully initialized Postgres instance with AGE and the bible graph ready to use. New developers on the project get a working graph environment without any manual installation steps.

What Comes Next

AGE is now installed, the graph is created, and the extension is verified. The next article covers how to write Cypher queries in Postgres — creating vertices and edges for Biblical entities, querying relationships, and the syntax differences between standard Cypher and AGE's SQL wrapper. Article 19 covers how to combine graph traversal with relational SQL queries in a single statement, which is where AGE's integration with Postgres becomes genuinely powerful.

Case Study

In Progress

Bible Verse — Case Study

Production SaaS Platform · Full-Stack · Founder & Sole Engineer

A domain-driven SaaS platform with five independently scalable system boundaries: scripture content delivery, RAG-backed AI study, real-time community interaction, async media processing, and infrastructure services — built and operated end-to-end.

Our Results

37K+
Verses Indexed
5
AI Models
5
Bounded Domains
3
Job Queues

How We Built It

  • RAG pipeline grounding AI responses in actual scripture rather than model memory
  • Hybrid Llama / OpenAI routing — local inference for cost, API fallback for quality at the edge
  • Non-blocking media processing — FFmpeg jobs enqueued via BullMQ, API never waits on transcoding
  • Cross-instance real-time consistency via Redis pub/sub behind WebSocket and WebRTC layers

Lessons Learned

  • Domain boundaries enforced at the service layer prevent coupling long before scale demands microservices.
  • RAG retrieval quality matters more than model size — better embeddings outperform a larger model on poor context.
  • Async queue design should be first-class, not bolted on; BullMQ worker isolation saved the request path repeatedly.

Stack

Nuxt 3TypeScriptNitroPostgreSQLPrismaRedisBullMQWeaviateMinIOFFmpegWebRTCWebSocketsLlama 3.2OpenAI APIKubernetes
View Full Case Study

Written by

Full-Stack Engineer & Systems Architect

5+ years building production systems · AI, Backend & Infrastructure · Founder of Bible Logic

Full-stack engineer with 5+ years of hands-on experience designing and shipping production systems — from Nuxt 3 frontends and Nitro APIs to self-hosted Kubernetes clusters, RAG pipelines, and real-time AI applications. Everything I write comes from systems I've designed, deployed, and operated in production.

5+ Years Experience AI Systems Specialist Kubernetes & Infrastructure
Nuxt 3TypeScriptPostgreSQLKubernetesRAG / LLMWebRTCAWS IVSRedis