Skip to main content

PostgreSQL

PostgreSQL is a powerful, open-source relational database system known for its reliability, feature robustness, and performance. This connector enables you to interact with PostgreSQL databases through PostgREST, which automatically generates a RESTful API from your database schema.

Prerequisites​

To use this connector, you need:

  1. A PostgreSQL database (version 9.6 or higher)
  2. PostgREST installed and configured
  3. A JWT token for authentication

What is PostgREST?​

PostgREST is a standalone web server that turns your PostgreSQL database directly into a RESTful API. It reads your database schema and creates API routes automatically, making it perfect for use with this MCP connector. PostgREST handles authentication, authorization through PostgreSQL's row-level security, and provides a powerful query interface.

Authentication Types​

PostgreSQL supports API Key authentication using JWT tokens:

  • API Key (JWT) - Uses JSON Web Tokens for secure, stateless authentication
    • Pros: Stateless, secure, industry standard, supports role-based access
    • Cons: Requires initial PostgREST setup and understanding of JWT
    • Best for: Production environments with proper security requirements

Setting up PostgREST​

1. Install PostgREST​

Choose one of the following installation methods:

Docker (Recommended for testing):

docker pull postgrest/postgrest

Binary Download:

Download from https://github.com/PostgREST/postgrest/releases

Package Managers:

# macOS
brew install postgrest

# Ubuntu/Debian
apt-get install postgrest

# Arch Linux
pacman -S postgrest

2. Configure PostgREST​

Create a postgrest.conf file in your project directory:

# Database connection
db-uri = "postgres://username:password@localhost:5432/database_name"

# Schema to expose via API (typically 'public' or a custom schema)
db-schemas = "public"

# Anonymous role for unauthenticated requests
db-anon-role = "web_anon"

# JWT secret for token validation (use a strong secret in production)
jwt-secret = "your-secret-key-minimum-32-characters-long"

# Server configuration
server-host = "*"
server-port = 3000

3. Set up Database Roles​

Create the necessary database roles in PostgreSQL:

-- Create anonymous role for API access
CREATE ROLE web_anon NOLOGIN;

-- Grant usage on schema
GRANT USAGE ON SCHEMA public TO web_anon;

-- Grant select on all tables (adjust permissions as needed)
GRANT SELECT ON ALL TABLES IN SCHEMA public TO web_anon;

-- For write operations, also grant INSERT, UPDATE, DELETE
GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO web_anon;

-- Grant usage on sequences (for auto-increment columns)
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO web_anon;

-- Optional: Create helper functions for schema introspection
-- These functions enable the List Tables, Describe Table, etc. tools

CREATE OR REPLACE FUNCTION postgrest_get_tables(schema_name text DEFAULT 'public', pattern text DEFAULT NULL)
RETURNS TABLE(table_name text, table_type text) AS $$
BEGIN
RETURN QUERY
SELECT t.table_name::text, t.table_type::text
FROM information_schema.tables t
WHERE t.table_schema = schema_name
AND (pattern IS NULL OR t.table_name ILIKE '%' || pattern || '%')
ORDER BY t.table_name;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE OR REPLACE FUNCTION postgrest_describe_table(table_name text)
RETURNS TABLE(
column_name text,
data_type text,
is_nullable text,
column_default text
) AS $$
BEGIN
RETURN QUERY
SELECT
c.column_name::text,
c.data_type::text,
c.is_nullable::text,
c.column_default::text
FROM information_schema.columns c
WHERE c.table_name = postgrest_describe_table.table_name
AND c.table_schema = 'public'
ORDER BY c.ordinal_position;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Grant execute permission on helper functions
GRANT EXECUTE ON FUNCTION postgrest_get_tables TO web_anon;
GRANT EXECUTE ON FUNCTION postgrest_describe_table TO web_anon;

4. Generate JWT Token​

You need a JWT token with a role claim to authenticate with PostgREST. Here's how to generate one:

Using jwt.io (Quick Testing):

  1. Go to https://jwt.io
  2. In the "PAYLOAD" section, use:
    {
    "role": "web_anon",
    "exp": 1735689600
    }
  3. In the "VERIFY SIGNATURE" section, paste your JWT secret
  4. Copy the encoded token from the left panel

Using Node.js:

const jwt = require('jsonwebtoken');

const token = jwt.sign(
{ role: 'web_anon' },
'your-secret-key-minimum-32-characters-long',
{ expiresIn: '7d' }
);

console.log(token);

Using Python:

import jwt
import datetime

token = jwt.encode(
{
'role': 'web_anon',
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7)
},
'your-secret-key-minimum-32-characters-long',
algorithm='HS256'
)

print(token)

5. Start PostgREST​

Using the binary:

postgrest postgrest.conf

Using Docker:

docker run -d \
-p 3000:3000 \
-v $(pwd)/postgrest.conf:/etc/postgrest.conf \
postgrest/postgrest

PostgREST will start on http://localhost:3000 by default.

Verify it's working:

curl http://localhost:3000/
# Should return a list of available tables

Configure in Willow​

  1. Go to Integrations → Add Connector → PostgreSQL
  2. Enter your PostgREST URL:
    • For local development: http://localhost:3000
    • For production: https://api.yourdomain.com (use HTTPS!)
  3. Select API Key authentication type
  4. Paste your JWT token in the Token field
  5. Click Connect

Common Use Cases​

Querying Data​

Ask natural language questions and the AI will translate them to PostgREST queries:

  • "Show me all users who registered last month"
  • "Get the top 10 products by sales"
  • "Find customers in New York with orders over $1000"
  • "What's the average order value by customer segment?"

Data Modification​

Make changes through conversation:

  • "Add a new product: name 'Widget X', price $29.99, category 'tools'"
  • "Update user john@example.com to set status as premium"
  • "Delete all sessions that expired before yesterday"
  • "Insert these 5 new customer records: [data]"

Schema Discovery​

Understand your database structure:

  • "What tables are in the database?"
  • "Describe the users table"
  • "Show me a sample of the orders table"
  • "What columns does the products table have?"

Working with Functions​

Call stored procedures and custom functions:

  • "List all available database functions"
  • "Call the calculate_discount function with price 100"
  • "Execute search_products function to find items containing 'laptop'"

Advanced Queries​

Leverage PostgreSQL's power:

  • "Search for 'artificial intelligence' in all blog post contents" (full-text search)
  • "Get posts with their author information in one query" (embedding)
  • "Show me distinct categories from the products table"
  • "Calculate the sum of all order totals grouped by month"

Troubleshooting​

Connection Refused​

Cause: PostgREST is not running or URL is incorrect

Solution:

  1. Verify PostgREST is running: curl http://localhost:3000/
  2. Check the PostgREST URL in the Setup tab
  3. Ensure firewall/network allows access to the PostgREST port
  4. If using Docker, verify port mapping is correct

401 Unauthorized​

Cause: Invalid or expired JWT token

Solution:

  1. Verify JWT secret matches between token and PostgREST config
  2. Check token expiration time (exp claim)
  3. Ensure token includes the correct role claim (should match db-anon-role)
  4. Generate a new token using the steps above
  5. Verify the token is properly formatted (should start with eyJ)

404 Table Not Found​

Cause: Table not in exposed schema or insufficient permissions

Solution:

  1. Check db-schemas in PostgREST config includes the schema containing your table
  2. Verify the database role has SELECT permission on the table:
    GRANT SELECT ON table_name TO web_anon;
  3. Restart PostgREST to refresh schema cache
  4. Check table name spelling (PostgreSQL is case-sensitive)

Permission Denied​

Cause: Database role lacks necessary permissions

Solution:

Grant appropriate permissions in PostgreSQL:

-- For reading data
GRANT SELECT ON table_name TO web_anon;

-- For inserting data
GRANT INSERT ON table_name TO web_anon;

-- For updating data
GRANT UPDATE ON table_name TO web_anon;

-- For deleting data
GRANT DELETE ON table_name TO web_anon;

-- For all operations
GRANT ALL PRIVILEGES ON table_name TO web_anon;

-- For sequences (auto-increment columns)
GRANT USAGE ON SEQUENCE table_name_id_seq TO web_anon;

After granting permissions, restart PostgREST.

Schema Cache Not Updating​

Cause: PostgREST caches schema information

Solution:

  1. Restart PostgREST to reload the schema cache
  2. Or send a SIGUSR1 signal to reload without restarting:
    killall -SIGUSR1 postgrest
  3. Or use the admin API (if enabled) to reload:
    curl -X POST http://localhost:3001/

CORS Errors (Browser Applications)​

Cause: PostgREST doesn't handle CORS by default

Solution:

Add to your postgrest.conf:

# Allow all origins (development only!)
server-cors-allowed-origins = "*"

# Or specify allowed origins
server-cors-allowed-origins = "https://yourdomain.com"

Or use a reverse proxy (nginx, Caddy) to handle CORS headers.

Advanced Configuration​

Row Level Security (RLS)​

Enable fine-grained access control using PostgreSQL's Row Level Security:

-- Enable RLS on a table
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

-- Create a policy (users can only see their own data)
CREATE POLICY user_policy ON users
FOR ALL
TO web_anon
USING (id = current_setting('request.jwt.claims', true)::json->>'user_id');

-- Grant permission
GRANT ALL ON users TO web_anon;

Custom Functions​

Create PostgreSQL functions for complex operations:

-- Simple calculation function
CREATE OR REPLACE FUNCTION calculate_discount(price numeric, discount_pct numeric)
RETURNS numeric AS $$
BEGIN
RETURN price * (1 - discount_pct / 100);
END;
$$ LANGUAGE plpgsql;

-- Search function
CREATE OR REPLACE FUNCTION search_products(query text)
RETURNS TABLE(id int, name text, price numeric) AS $$
BEGIN
RETURN QUERY
SELECT p.id, p.name, p.price
FROM products p
WHERE p.name ILIKE '%' || query || '%'
OR p.description ILIKE '%' || query || '%';
END;
$$ LANGUAGE plpgsql;

-- Grant execute permission
GRANT EXECUTE ON FUNCTION calculate_discount TO web_anon;
GRANT EXECUTE ON FUNCTION search_products TO web_anon;

Call via MCP: "Calculate discount for price 100 with 20% off" or "Search for products containing 'laptop'"

Views for Simplified Access​

Create views to simplify complex queries and hide implementation details:

-- View for active customers with their order count
CREATE VIEW active_customers AS
SELECT
c.id,
c.name,
c.email,
COUNT(o.id) as order_count,
SUM(o.total) as total_spent
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE c.status = 'active'
GROUP BY c.id, c.name, c.email;

-- Grant permission
GRANT SELECT ON active_customers TO web_anon;

Full-Text Search Setup​

Create indexes for efficient full-text search:

-- Add a tsvector column
ALTER TABLE articles ADD COLUMN content_search tsvector;

-- Create index
CREATE INDEX articles_content_search_idx ON articles USING GIN(content_search);

-- Update the tsvector column (one-time)
UPDATE articles SET content_search = to_tsvector('english', title || ' ' || content);

-- Auto-update trigger
CREATE TRIGGER articles_content_search_update
BEFORE INSERT OR UPDATE ON articles
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(content_search, 'pg_catalog.english', title, content);

Performance Optimization​

  1. Create Indexes on frequently queried columns:

    CREATE INDEX idx_users_email ON users(email);
    CREATE INDEX idx_orders_customer_id ON orders(customer_id);
    CREATE INDEX idx_products_category ON products(category);
  2. Use Connection Pooling with pgBouncer for high-traffic applications

  3. Optimize Queries by selecting only needed columns instead of *

  4. Set Statement Timeout in PostgREST config:

    db-pool-timeout = 10
    db-pre-request = "SET statement_timeout = 5000"

Security Best Practices​

  1. Use HTTPS in production - never send JWT tokens over HTTP
  2. Rotate JWT secrets regularly and use strong, random secrets (32+ characters)
  3. Set token expiration - don't create tokens that never expire
  4. Use Row Level Security to enforce data access policies at the database level
  5. Grant minimal permissions - only give roles the permissions they need
  6. Validate input using PostgreSQL constraints and triggers
  7. Use prepared statements - PostgREST does this automatically
  8. Monitor access logs to detect unusual activity
  9. Keep PostgREST updated to get security patches
  10. Never expose your admin role - use separate roles for API access

Additional Resources​