n8n PostgreSQL Integration: Connecting Your Database to Automated Workflows
How to connect n8n to Postgres, pick the right node (Postgres or Postgres Trigger), get SSL working on n8n Cloud and self-hosted, and keep the workflow correct on its second, tenth, and thousandth run.
The n8n Postgres node (n8n-nodes-base.postgres) lets a workflow read from and write to any PostgreSQL database. You add a Postgres credential (host, port, database, user, password, SSL mode), drop the node on the canvas, and pick an operation: Execute Query, Select, Insert, Insert or Update, Update, or Delete. A separate node, the Postgres Trigger, starts a workflow when a row changes.
That part takes about ten minutes. The part most guides skip is what happens after the first successful test. A scheduled workflow that inserts 40 rows on Monday inserts the same 40 rows again on Tuesday. A SELECT that comes back empty quietly stops everything downstream. A COUNT(*) that returns "42" (a string, not a number) breaks the IF node that was supposed to compare it.
This guide covers the setup, and then it covers the part that keeps the workflow honest a year from now. We call that part the Run-Twice Test:
- Same state on the second run. Running the workflow twice with the same input leaves the database exactly as one run would. No duplicate rows.
- Zero rows is a valid answer. An empty result is handled on purpose, not by accident.
- Numbers arrive as numbers. NUMERIC and BIGINT values are typed before any math or comparison touches them.
Every production section below maps to one of those three rules. If a Postgres workflow fails any of them, it will eventually corrupt data or stop without telling you.
We recommend n8n for most small business automation. Affiliate link: we earn a small commission at no cost to you.
Written by Ryan Nichols, Founder and CTO at Rain Automation. I build and maintain n8n workflows in production for small businesses, and our own n8n instance runs on Postgres.
- Use the Postgres node for anything your workflow does to the database (read, write, run SQL). Use the Postgres Trigger node only when a database change should start the workflow, and only if your database user can create functions and triggers.
- n8n Cloud has no field for a custom CA certificate. Against a managed database with its own certificate chain, the only Cloud option that connects is "Ignore SSL Issues (Insecure)", which encrypts the connection but skips verifying who is on the other end. Self-hosted n8n can trust the real CA by mounting it into
/opt/custom-certificates. - Plain INSERT on a scheduled workflow creates duplicates on every rerun. Use
INSERT ... ON CONFLICTkeyed on a stable external ID (the ID from the source system), never on your own auto-increment column. - n8n only passes data forward when a node outputs at least one item. An empty SELECT ends the branch silently unless you turn on Always Output Data and handle the empty case.
- The Postgres node returns NUMERIC and BIGINT columns as text by default (the "Output Large-Format Numbers As" option).
COUNT(*)andSUM()return BIGINT or NUMERIC, so they arrive as strings. Cast in SQL or change the option.
Quick Answer: How do I connect n8n to PostgreSQL?
- Create a database user for n8n with only the privileges the workflow needs (see the SQL below).
- Add a Postgres credential: In n8n, go to Credentials > New > Postgres. Enter Host, Database, User, Password, Port (5432 by default), and set SSL to Require for any database outside your own private network.
- Add the Postgres node, select the credential, choose Execute Query, and run
SELECT 1;to confirm the connection. - Before you schedule it, make sure writes are upserts, empty results are handled, and numeric fields are cast.
Postgres Node vs Postgres Trigger Node: Which One You Need
The five-second test: does the database start the workflow, or does the workflow use the database?
n8n ships two different Postgres nodes, and they solve opposite problems. The Postgres node is an action: something else starts the workflow (a schedule, a webhook, a form), and the node reads or writes the database along the way. The Postgres Trigger node is a starting point: a row is inserted, updated, or deleted, and that event kicks off the workflow.
Here's the five-second test. If you can finish the sentence "Every time a row changes in this table, do X," you might want the trigger. Anything else is the regular Postgres node.
| Postgres node | Postgres Trigger node | |
|---|---|---|
| Role | Action inside a workflow | Starts a workflow |
| What it does | Execute Query, Select, Insert, Insert or Update, Update, Delete | Fires on INSERT, UPDATE, or DELETE on one table, or listens on an existing channel ("Advanced" mode) |
| How it works | Opens a connection, runs SQL, returns rows as items | Creates a PL/pgSQL function and a row-level trigger that calls pg_notify, then holds a LISTEN connection open |
| Database privileges | Whatever the SQL needs (often just SELECT/INSERT/UPDATE on specific tables) | Rights to create a function in the schema and a trigger on the table (table owner, or TRIGGER privilege plus CREATE on the schema) |
| Works through a transaction-mode connection pooler | Yes | No. LISTEN needs a persistent session |
| Good fit | Scheduled syncs, writing form submissions, enrichment lookups, reports | Reacting to rows your own app writes, when polling would be too slow |
Which operation inside the Postgres node?
The structured operations (Select, Insert, Insert or Update, Update, Delete) build the SQL for you from a table picker and column mappings. They're the right call when you're mapping fields from an upstream node into one table.
Execute Query runs SQL you write. It's the right call the moment you need a join, a CTE, a custom ON CONFLICT clause, or a cast. In our own instance, 24 of the 25 Postgres nodes in active workflows use Execute Query. That isn't a rule, but it tells you where most real workflows end up.
Three things that bite people about the Postgres Trigger node
- It writes to your schema. The node creates a function and a trigger on the table when the workflow activates. Your app's database user usually can't do that, and a DBA may not want it to. Ask first.
- It can break your app's writes. The trigger sends each row as JSON through
pg_notify. Postgres caps a NOTIFY payload at 8,000 bytes by default. If a row is wider than that (a long notes field, a JSON blob), the notify call raisespayload string too long, and because the trigger runs inside your app's transaction, the INSERT itself fails. That's a workflow tool causing a production outage in a different system. Keep triggered tables narrow, or use "Advanced" mode with your own trigger function that sends only the row ID. - It needs a direct or session-mode connection. Managed providers often hand you a pooled connection string first. Supabase's transaction pooler (port 6543) can't hold a
LISTEN. Use the direct connection or the session pooler for the trigger credential.
If any of those are a problem, the fallback is simple and dependable: a Schedule Trigger every minute plus a Postgres node that selects rows where updated_at is newer than the last run. It's less elegant. It also never takes down your app.
Before You Connect: Give n8n Its Own Database User
Least privilege, and the fix for "permission denied for schema public".
Don't connect n8n as postgres or as your app's owner account. Workflow credentials get shared, copied into new workflows, and used by people who weren't there when you set them up. A dedicated user limits what a bad query (or a bad actor with access to your n8n instance) can do.
Here's a starting point for a user that can read and write two specific tables and nothing else:
CREATE ROLE n8n_app WITH LOGIN PASSWORD 'use-a-long-random-password';
GRANT CONNECT ON DATABASE your_database TO n8n_app;
GRANT USAGE ON SCHEMA public TO n8n_app;
GRANT SELECT, INSERT, UPDATE ON public.leads, public.lead_events TO n8n_app;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO n8n_app;
Add DELETE only if a workflow genuinely deletes. Add CREATE on the schema only if a workflow creates tables (most shouldn't).
Fixing "permission denied for schema public"
This error usually shows up on PostgreSQL 15 or newer. Starting with version 15, Postgres no longer lets every user create objects in the public schema by default. Tutorials written for older versions skip the grant because it used to be automatic.
The fix depends on what the error is blocking. If the user can't read or write tables, it's missing USAGE on the schema plus table privileges (the grants above). If it's failing to create a table or a trigger function, it also needs:
GRANT CREATE ON SCHEMA public TO n8n_app;
You'll see guides suggest GRANT ALL PRIVILEGES ON SCHEMA public. It works, and it's more access than a workflow needs. Grant the specific privilege that's failing.
Two Postgres connections that have nothing to do with each other
If you self-host n8n on Postgres (we do), you have two separate database connections. The DB_POSTGRESDB_* environment variables in your docker-compose.yml tell n8n where to store its own workflows and execution history. The Postgres credential you create in the n8n UI is what your workflows use to reach your business data.
Changing one never affects the other. When something breaks, first ask which connection is failing. (If that sounds familiar, it's the same split we cover for email in our n8n SMTP configuration guide: system settings in environment variables, workflow settings in credentials.)
Step 1: Create the Postgres Credential in n8n
Every field, what it does, and the Docker networking mistake behind most ECONNREFUSED errors.
In n8n, go to Credentials > New Credential > Postgres. These are the fields you'll see in current n8n (we're on version 2.42):
| Field | Default | What to enter |
|---|---|---|
| Host | localhost |
The database hostname. In Docker, this is almost never localhost (see below). |
| Database | postgres |
Your database name. |
| User | postgres |
The dedicated n8n user you created above. |
| Password | (empty) | That user's password. |
| Maximum Number of Connections | 100 | Lower it (10 to 20 is plenty for most small workloads) on managed databases with tight connection limits. |
| Ignore SSL Issues (Insecure) | Off | Leave off unless you have no other option. Covered in the SSL section. |
| SSL | Disable | Require for anything that crosses the internet. Allow and Disable are for a database on the same private network. |
| Port | 5432 | Your provider's port. Pooled connections often use a different one. |
| SSH Tunnel | Off | Turn on to reach a database that isn't exposed publicly, through a bastion host. |
Click Save. n8n tests the connection when you save, and the error message it shows is the first thing to read when it fails.
Why "localhost" fails when n8n runs in Docker
Inside a container, localhost means the container itself. If Postgres runs on the host machine or in a different container, n8n tries to connect to itself and gets ECONNREFUSED 127.0.0.1:5432. That's a networking problem, not an n8n problem.
- Postgres in the same Docker Compose project: use the service name as the host (for example
postgresordb). Containers on the same Compose network resolve each other by name. - Postgres in a different Compose project: put both on a shared Docker network and use the container name. Our n8n reaches its own database this way.
- Postgres installed directly on the host (Linux): add
extra_hosts: ["host.docker.internal:host-gateway"]to the n8n service, usehost.docker.internalas the host, and make sure Postgres listens on an address the container can reach (listen_addressesin postgresql.conf, plus a matchingpg_hba.confrule).
One more: if Postgres starts in the same Compose file, it can still be initializing when n8n boots. Add a healthcheck to the Postgres service and depends_on with condition: service_healthy on n8n, so the first connection attempt doesn't fail on a cold start.
SSL on n8n Cloud vs Self-Hosted: The Fork Nobody Mentions
Why the same credential connects on one and fails on the other, and what each fix actually costs you.
Managed Postgres providers (Supabase, AWS RDS, DigitalOcean, and others) often sign their server certificates with their own certificate authority, not one that's in the default trust store. When n8n connects with SSL set to Require and verification on, Node.js checks that chain, doesn't recognize the CA, and refuses with an error like self-signed certificate in certificate chain.
The fix is to tell n8n to trust that CA. And this is where n8n Cloud and self-hosted n8n split.
| n8n Cloud | Self-hosted n8n | |
|---|---|---|
| CA certificate field in the Postgres credential | None | None (same credential) |
| Can you add a CA to the trust store? | No. You don't control the container | Yes. Mount it into /opt/custom-certificates |
| Option that connects to a private-CA database | SSL: Require + Ignore SSL Issues (Insecure) | SSL: Require, with verification on |
| What you get | Encrypted traffic, server identity not verified | Encrypted traffic, server identity verified |
What "Ignore SSL Issues" actually means
It doesn't turn off encryption. Your data still travels over TLS. What it turns off is the check that the server on the other end is really your database. Without that check, someone who can intercept the network path can present their own certificate, and n8n will accept it.
For a lot of small internal workflows, that risk is acceptable and people take it knowingly. For a workflow moving customer records, payment data, or anything regulated, I wouldn't. (I spent 15 years in cybersecurity before building automation, and "encrypted but unverified" is exactly the kind of setting that looks fine until an audit.)
If you're on n8n Cloud and keep this setting, reduce the blast radius:
- Use the dedicated, least-privilege user from the prerequisites section.
- If your provider supports IP allowlisting, restrict the database to the addresses that need it.
- Keep sensitive tables out of the workflow user's grants entirely.
The self-hosted fix: trust the real CA
The official n8n Docker image checks for a directory at /opt/custom-certificates when it starts. If the directory exists, it adds every certificate in it to the trust store. Download your provider's CA certificate (Supabase, RDS, and DigitalOcean all publish one) and mount it:
services:
n8n:
image: n8nio/n8n
volumes:
- n8n_data:/home/node/.n8n
- ./certs:/opt/custom-certificates:ro
Restart with docker compose down && docker compose up -d. Then set the credential's SSL to Require, leave Ignore SSL Issues off, and save. If the save succeeds, n8n verified the chain.
If you run queue mode, mount the same directory on every worker container too. Workers execute the workflows, so a worker without the certificate fails the same connection the main instance passes. It's the same parity rule we hit with SMTP settings in queue mode, and it fails just as quietly.
When the connection drops after an upgrade
A pattern that comes up repeatedly in the n8n community forum: a Postgres node that worked for months starts throwing Connection refused or Connection terminated unexpectedly right after an n8n version upgrade. In the threads we read, the root cause was frequently the SSH tunnel in front of the database rather than the Postgres node. If you tunnel, test the tunnel on its own after every upgrade, and pin your n8n image to a specific version (we do) so upgrades happen when you choose, not when the container restarts.
Step 2: Run Your First Query (With Parameters, Not String Building)
The habit that prevents SQL injection from form inputs and webhook payloads.
Add a Postgres node, pick your credential, and choose Execute Query. Run this first to confirm everything works:
SELECT current_user, version();
Now the habit that matters. It's tempting to drop an n8n expression straight into the SQL, like WHERE email = '{{ $json.email }}'. Don't. If that email came from a web form or a webhook, whoever filled it in now controls part of your query.
Use Query Parameters instead. In the node's Options, add Query Parameters and list the values, comma-separated. Reference them in the SQL as $1, $2, and so on:
SELECT id, status, assigned_to
FROM leads
WHERE email = $1 AND created_at > $2;
Query Parameters: {{ $json.email }}, {{ $json.since }}
Postgres treats each parameter as a value, never as SQL, so a malicious input can't change what the query does. If a value contains a comma, wrap it with an expression that returns an array instead of a comma-separated string, so n8n doesn't split it.
Need this running in your business? We build and maintain n8n and Postgres workflows for small businesses, including the dedup, error handling, and monitoring this tutorial covers next. Get in touch.
What Breaks on the Second Run (and How to Fix Each One)
The three Run-Twice Test rules, with the exact SQL and node settings.
Your workflow passed its first test. Here's what happens next, in the order most people discover it.
Rule 1: Same state on the second run (duplicate rows)
Picture a workflow that runs every hour, pulls new leads from a form tool's API, and inserts them into your leads table. The API returns "leads from the last 24 hours" because that's the simplest filter it offers. By the end of the first day, every lead is in your table 24 times.
The fix is an upsert keyed on a stable external ID: the ID the source system assigned, not your table's own id column. Your auto-increment ID is created at insert time, so it can never tell you a row already exists. The source system's ID can.
First, make that column unique. ON CONFLICT only works against a unique constraint or index; without one, Postgres rejects the query with there is no unique or exclusion constraint matching the ON CONFLICT specification.
ALTER TABLE leads ADD CONSTRAINT leads_source_id_key UNIQUE (source_id);
Then write the insert so a repeat updates instead of duplicating:
INSERT INTO leads (source_id, email, name, status, updated_at)
VALUES ($1, $2, $3, $4, now())
ON CONFLICT (source_id) DO UPDATE
SET email = EXCLUDED.email,
name = EXCLUDED.name,
status = EXCLUDED.status,
updated_at = now();
EXCLUDED refers to the row you tried to insert. Run it once, you get one row. Run it fifty times, you still get one row, with the latest values.
You have three ways to get this behavior in n8n, from least to most control:
- Insert with "Skip on Conflict" on. n8n adds
ON CONFLICT DO NOTHING. Duplicates are ignored, but changed records never update. Fine for append-only event logs. - Insert or Update operation. n8n builds the
ON CONFLICT (column) DO UPDATEfor you from the column you pick to match on. Good for straightforward one-table syncs. - Execute Query with your own SQL (above). Use this when you need to update only some columns, keep a
first_seen_atthat never changes, or skip updates when nothing changed.
Watch for the one that catches people: if the same batch contains two items with the same external ID, a single multi-row upsert fails with ON CONFLICT DO UPDATE command cannot affect row a second time. Dedupe the items before the Postgres node (the Remove Duplicates node works) or let n8n run the query once per item.
Rule 2: Zero rows is a valid answer (the silent stop)
n8n passes data to the next node only when the current node outputs at least one item. If your Postgres node runs SELECT ... WHERE status = 'overdue' and nothing is overdue today, the node outputs zero items and everything after it simply doesn't run. The execution shows as successful. Nothing errors. Nothing alerts.
We cover this exact blind spot, along with four other ways n8n workflows fail without telling you, in this guide.
Most of the time that's what you want. "No overdue invoices, so don't send reminder emails" is correct behavior. It becomes a problem when a later step must run regardless, like writing a "sync completed" timestamp, closing out a report, or posting "nothing to do today" to Slack.
For those workflows, open the Postgres node's Settings and turn on Always Output Data. The node now emits a single empty item when there are no rows. Follow it immediately with an IF node that checks whether a real field exists (for example {{ $json.id }} is not empty), and route the two cases separately.
Don't turn it on everywhere. An empty item flowing into a Send Email node sends an email with blank fields, which is worse than sending nothing. (Our n8n Send Email node tutorial covers wiring email into workflows like this.) In our own instance, 2 of 25 Postgres nodes in active workflows have it on, both deliberately.
Rule 3: Numbers arrive as numbers (NUMERIC and BIGINT as strings)
JavaScript numbers lose precision past about 16 digits. Postgres BIGINT and NUMERIC can hold far more than that. To avoid silently rounding your data, the n8n Postgres node returns these column types as text by default. The setting is under Options as Output Large-Format Numbers As, and it defaults to Text.
That sounds like an edge case until you notice which everyday functions return BIGINT or NUMERIC:
COUNT(*)returns BIGINT, so a count of 42 arrives as"42".SUM()of an integer column returns BIGINT.SUM()of a NUMERIC column returns NUMERIC.- Any BIGINT primary key (common on larger tables) arrives as text.
- Money columns stored as NUMERIC arrive as text.
So an IF node checking "count is greater than 10" either errors on a type mismatch or compares text. And a Code node adding two "numbers" gets "4250" from "42" + "50".
Fix it at the right layer:
- Counts and IDs that will never exceed about 2 billion: cast in SQL.
SELECT COUNT(*)::int AS open_leads FROM leads WHERE status = 'open'; - Larger values that still fit in 16 digits: switch Output Large-Format Numbers As to Numbers on that node.
- Money: keep it as text through the workflow and do the math in SQL, or store amounts as integer cents. Floating-point math on currency is a bug you'll find at reconciliation time.
How Postgres Runs Inside Our Own n8n Instance
Real counts from our production stack, as of October 2026.
We don't just recommend this setup. We run on it.
Our n8n instance stores its own data in PostgreSQL 17 (we moved it off SQLite in May 2026), and Postgres is the most common thing our workflows touch. Here's the current picture from a query against our production instance:
| Measure | Count |
|---|---|
| Workflows in the instance | 80 |
| Active workflows | 26 |
| Workflows containing a Postgres node | 32 |
| Active workflows containing a Postgres node | 11 |
| Postgres nodes in active workflows | 25 |
| Of those, using Execute Query | 24 |
| Of those, using Always Output Data | 2 |
| Workflows using the Postgres Trigger node | 0 |
A few things stand out. Almost every node uses hand-written SQL, because real workflows need joins, casts, and custom conflict handling. We don't use the Postgres Trigger node at all; our apps write the rows, so the app fires a webhook or the workflow polls on a schedule, and neither can break an application write. And Always Output Data is the exception, not the default.
Four in ten of the workflows we keep active depend on Postgres. That's why the Run-Twice Test isn't theoretical for us. A duplicate-row bug in one of those workflows would show up in a client report, not just a log.
n8n Postgres Errors: What They Mean and How to Fix Them
Match the error text to the cause.
| Error or symptom | Likely cause | Fix |
|---|---|---|
ECONNREFUSED 127.0.0.1:5432 |
n8n is in Docker and Host is localhost |
Use the Compose service name, a shared network, or host.docker.internal |
ECONNREFUSED only on container startup |
Postgres still initializing when n8n boots | Healthcheck on Postgres plus depends_on: condition: service_healthy |
self-signed certificate in certificate chain |
Provider uses its own CA | Self-hosted: mount the CA in /opt/custom-certificates. Cloud: Ignore SSL Issues, with the tradeoffs above |
no pg_hba.conf entry for host |
Server rejects your address or requires SSL | Add a pg_hba.conf rule for n8n's address, or set SSL to Require |
permission denied for schema public |
Missing USAGE or CREATE on the schema (default since Postgres 15) | Grant the specific privilege that's failing |
permission denied for table leads |
User lacks table privileges | GRANT SELECT, INSERT, UPDATE ON leads TO n8n_app; |
there is no unique or exclusion constraint matching the ON CONFLICT specification |
Upsert column isn't unique | Add a unique constraint on the external ID |
ON CONFLICT DO UPDATE command cannot affect row a second time |
Same key appears twice in one batch | Remove duplicates before the Postgres node |
payload string too long in your app's logs |
Postgres Trigger row exceeds the 8,000-byte NOTIFY limit | Narrow the table, or send only the row ID from a custom trigger |
Connection terminated unexpectedly after an upgrade |
Often the SSH tunnel, not the node | Test the tunnel alone, pin your n8n version |
| Workflow "succeeds" but later nodes never run | Query returned zero rows | Always Output Data plus an IF node, where a downstream step must run |
IF node errors on a type mismatch, or math gives "4250" |
BIGINT/NUMERIC returned as text | Cast in SQL or set Output Large-Format Numbers As to Numbers |
| Duplicate rows after every scheduled run | Plain INSERT | Upsert on a stable external ID |
n8n Postgres Production Checklist
Run through this before you turn on the schedule.
- n8n connects with a dedicated user, not
postgresor the app owner - That user has only the table privileges its workflows use
- SSL is set to Require for any database reached over the internet
- Self-hosted: provider CA is mounted in
/opt/custom-certificateson the main instance and every worker - n8n Cloud: if Ignore SSL Issues is on, the database is IP-restricted and the user is least-privilege
- Every user-supplied value goes through Query Parameters, never string-built SQL
- Every scheduled write is an upsert keyed on a stable external ID with a unique constraint
- Batches are deduplicated before multi-row upserts
- Each Postgres node has a deliberate answer to "what if this returns zero rows?"
- Every COUNT, SUM, BIGINT, and NUMERIC field is cast or explicitly handled
- The workflow has an error workflow attached, so a failure sends an alert instead of disappearing
- You ran the workflow twice with the same input and the database looked identical both times
The tutorial gets you a working Postgres node. The harder part is everything around it. The upsert logic that survives a source system changing its IDs. The alert that fires when a nightly sync returns zero rows three nights in a row. The retry when the database restarts for maintenance at 2 AM on a Saturday. The schema change that doesn't break the eleven workflows reading that table.
We build and maintain n8n and Postgres systems for small businesses, and we run the same stack for our own products. Get in touch.
Frequently Asked Questions
How do I connect n8n to a PostgreSQL database?
Create a Postgres credential in n8n (Credentials > New > Postgres) with your host, port, database, user, and password, and set SSL to Require for any database outside your private network. Then add a Postgres node to a workflow, select the credential, and run a test query like SELECT 1;. If n8n runs in Docker, use the database's service or container name as the host, not localhost.
What is the difference between the Postgres node and the Postgres Trigger node in n8n?
The Postgres node is an action that reads from or writes to the database while a workflow runs. The Postgres Trigger node starts a workflow when a row is inserted, updated, or deleted. The trigger works by creating a function and a row-level trigger that sends pg_notify events, so it needs higher database privileges and a direct or session-mode connection.
Why does my n8n Postgres node create duplicate rows?
Because a plain INSERT adds a new row every time the workflow runs, including reruns over the same data. Use the Insert or Update operation, or write INSERT ... ON CONFLICT (source_id) DO UPDATE, keyed on a stable ID from the source system. The conflict column needs a unique constraint.
Why does my n8n workflow stop after a Postgres node with no error?
The query returned zero rows, and n8n only runs the next node when the previous one outputs at least one item. If a later step must run anyway, turn on Always Output Data in the Postgres node's settings and add an IF node to handle the empty case.
Why does n8n return numbers from Postgres as strings?
The Postgres node returns NUMERIC and BIGINT columns as text by default, because JavaScript can't represent numbers beyond about 16 digits exactly. COUNT(*) and SUM() return these types, so their results arrive as strings. Cast in SQL (COUNT(*)::int) or set the node option Output Large-Format Numbers As to Numbers.
Can I use a custom CA certificate for Postgres SSL on n8n Cloud?
No. The Postgres credential has no CA certificate field, and on n8n Cloud you can't add certificates to the container. For databases signed by a private CA, the Cloud option is Ignore SSL Issues (Insecure), which keeps encryption but skips server verification. Self-hosted n8n can trust the CA properly by mounting it in /opt/custom-certificates.
How do I fix "permission denied for schema public" in n8n?
Grant the n8n database user the schema privilege it's missing. Reading and writing tables needs GRANT USAGE ON SCHEMA public plus table privileges. Creating tables or trigger functions also needs GRANT CREATE ON SCHEMA public. PostgreSQL 15 and newer no longer grant CREATE on public to every user by default.
Can n8n connect to Supabase Postgres?
Yes. Use the connection details from your Supabase project's database settings with SSL set to Require. The regular Postgres node works with the direct connection or either pooler. The Postgres Trigger node needs the direct connection or the session pooler, because the transaction pooler can't hold a LISTEN session.
What to Build Next
With Postgres connected and passing the Run-Twice Test, you have a reliable place for your workflows to keep state: lead records, sync checkpoints, job history, and the memory an AI agent needs between runs.
If you're sending email from those records, start with our n8n Send Email node tutorial, then make it production-grade with the n8n SMTP configuration guide. If you're still deciding whether n8n is the right platform at all, our n8n vs Make comparison for small businesses covers where each one wins, including why self-hosting matters once your workflows touch your own database. And for the hosting side, Self-Hosting n8n in Production: Real Server Specs and Cost Numbers covers what the infrastructure actually costs.
When a database plus a few workflows starts to look like the core of how your business runs, it may be time to stop stitching tools together. Our custom development page covers what a purpose-built platform looks like, and when it makes sense.
Ready to try n8n? Sign up for a free account and connect your first database.
Building something more complex than a tutorial can cover?
A nightly sync that pulls jobs from your booking tool into your own database. A lead table that three systems write to without stepping on each other. A reporting workflow that has to be right every Monday because the owner reads it before the team meeting.
These are the workflows we build and maintain for service businesses every week. If your project has outgrown what a tutorial can teach you, describe what you need. We'll tell you how we'd build it.
Related Reads
More on n8n and business automation:
- n8n Send Email Node: SMTP and Gmail Setup Guide: Send email from the records your Postgres workflows produce, with SMTP setup for Gmail, Outlook, and custom domains.
- n8n SMTP Configuration for Production Workflows: The system-level vs workflow-level split, queue-mode parity, and exact settings for four production providers.
- n8n vs Make for Small Business Automation: A side-by-side comparison for small businesses choosing an automation platform, including the self-hosting and data-control tradeoffs.
This guide is for informational purposes. n8n node options, PostgreSQL defaults, and provider SSL requirements change over time. Field names and defaults here were checked against n8n 2.42 in October 2026. Always confirm against the official n8n Postgres node documentation and Postgres Trigger node documentation.
Disclosure: Some links in this post are affiliate links. If you sign up through them, Rain Automation may receive a commission at no additional cost to you. We only recommend tools we use in production ourselves.