Google added PostgreSQL JDBC support to Apps Script in 2026. Here's the exact connection string format for Supabase, Neon, Heroku, Railway, RDS, and Cloud SQL.
Google Apps Script's JDBC service added PostgreSQL support in early 2026, documented by Justin Poehnelt, a Senior Software Engineer at Google. The key catch: every PostgreSQL provider gives you a postgres:// URI, and Apps Script does not accept that format. You must convert it to jdbc:postgresql:// with credentials moved from the URL inline to query parameters. This guide covers the exact conversion, working connection strings for every major PostgreSQL host, JSONB and UUID type casting, SSL configuration, and the failure modes no other guide covers yet.
Background: What Google Added and Why It Matters
For years, Google Apps Script's JDBC service officially supported MySQL, Microsoft SQL Server, Oracle, and Google Cloud SQL (MySQL). PostgreSQL was conspicuously absent: despite PostgreSQL being the dominant database under most modern SaaS products, cloud-native startups, and managed database providers.
In February 2026, Justin Poehnelt (a Senior Software Engineer at Google) published documentation confirming that Jdbc.getConnection() now accepts jdbc:postgresql:// connection strings. The Apps Script documentation has been updated to reflect PostgreSQL as a supported database type.
This matters for two reasons:
- No middleware required. Teams connecting Supabase, Neon, Heroku, Railway, or RDS PostgreSQL to Google Sheets no longer need a third-party connector, an API layer, or a Python script running on a separate server. Apps Script can talk directly to the database.
- First-mover window. There are essentially no existing guides for most provider-specific configurations. The general connection string format is documented; the provider-specific gotchas (Supabase's pooler requirement, Neon's cold start behavior, SSL parameter differences between providers) are not. This guide is that resource.
The Problem: postgres:// vs. jdbc:postgresql://
Every PostgreSQL provider (Supabase, Neon, Heroku, Render, Railway, Cloud SQL, RDS) gives you a connection string in this format:
postgres://username:[email protected]:5432/database?sslmode=require
Some providers use postgresql:// as the protocol prefix instead of postgres://. Both are the modern URI format specified in the PostgreSQL documentation.
Neither format works in Apps Script. Pasting either into Jdbc.getConnection() produces an unhelpful error because Apps Script uses a Java-based JDBC driver that predates the modern URI format. JDBC has its own connection string convention that is structurally different.
"I spent an hour thinking my credentials were wrong before I realized the problem was the
postgres://prefix. The error message says nothing about the format." (r/googleappsscript)
"Copied the connection string straight from Supabase's dashboard. Got 'Failed to establish a database connection.' Turns out the format is completely wrong for Apps Script. There's exactly one blog post about this and it's from the engineer who shipped the feature." (Hacker News)
The format mismatch is the single most common reason PostgreSQL connections fail in Apps Script. Once you understand the conversion, everything else follows.
The Conversion Formula
Here is the full breakdown of what changes between the modern URI format and the JDBC format Apps Script requires:
| Component | Modern Format (what providers give you) | Apps Script JDBC Format |
|---|---|---|
| Protocol | postgres:// or postgresql:// | jdbc:postgresql:// |
| Credentials | Inline: user:password@host | Query parameters: ?user=x&password=y |
| Port | Often implicit (defaults to 5432) | Must be explicit: :5432 |
| SSL | ?sslmode=require | ?ssl=true |
| SSL mode | sslmode=verify-full, sslmode=prefer | Not supported: use ssl=true only |
Modern format (what your provider gave you):
postgres://alice:[email protected]/myapp?sslmode=require
Apps Script JDBC format (what you need):
jdbc:postgresql://db.example.com:5432/myapp?user=alice&password=s3cr3t&ssl=true
The port must be explicit. The credentials move out of the URL path and into query parameters. The sslmode parameter is dropped in favor of ssl=true.
Connection Setup: The Base Code
Store your JDBC URL in Script Properties (Project Settings > Script Properties in the Apps Script editor). Never hardcode credentials in your script source.
/**
* CONFIGURATION
* Set 'DB_URL' in Project Settings > Script Properties.
*
* Format:
* jdbc:postgresql://HOST:5432/DATABASE?user=USER&password=PASS&ssl=true
*
* Example:
* jdbc:postgresql://db.example.com:5432/myapp?user=alice&password=s3cr3t&ssl=true
*/
function getDbConnection() {
const dbUrl = PropertiesService
.getScriptProperties()
.getProperty('DB_URL');
if (!dbUrl) {
throw new Error('DB_URL Script Property is not set. ' +
'Go to Project Settings > Script Properties.');
}
return Jdbc.getConnection(dbUrl);
}
/**
* Basic connectivity test - run this first to confirm your
* connection string, SSL, and credentials are all correct.
*/
function testConnection() {
let conn;
try {
conn = getDbConnection();
const stmt = conn.createStatement();
const results = stmt.executeQuery('SELECT version()');
if (results.next()) {
Logger.log('Connection successful. PostgreSQL version: ' +
results.getString(1));
}
results.close();
stmt.close();
} catch (e) {
Logger.log('Connection failed: ' + e.message);
throw e;
} finally {
if (conn) conn.close();
}
}Run testConnection() first. A successful log output confirms SSL, credentials, and network connectivity are all working before you write any more code.
Provider-Specific Connection Strings
Supabase
Supabase gives you two types of connection strings: a direct connection and a pooler connection. Use the pooler.
Why: Apps Script triggers can fire from many servers simultaneously and do not reuse connections. Direct Postgres connections have a connection count limit (typically 60–100 for Supabase Free/Pro). The connection pooler (PgBouncer) handles connection multiplexing and prevents "too many connections" errors that will appear within days on any busy trigger setup.
Supabase pooler connection string (from your Supabase dashboard > Project Settings > Database > Connection pooling):
postgres://postgres.REFERENCE_ID:[email protected]:6543/postgres?pgbouncer=true
Convert to JDBC format:
jdbc:postgresql://aws-0-REGION.pooler.supabase.com:6543/postgres?user=postgres.REFERENCE_ID&password=PASSWORD&ssl=true
Critical: use port 6543, not 5432. Port 5432 is the direct connection. Port 6543 is the pooler. The pooler URL also contains your project reference ID as part of the username (postgres.abcdefghijklmnop).
// Example: Supabase connection in Script Properties // Key: DB_URL // Value: jdbc:postgresql://aws-0-us-east-1.pooler.supabase.com:6543/postgres?user=postgres.abcdefghijklm&password=YOUR_DB_PASSWORD&ssl=true
Note: Supabase also offers a Session mode pooler on port 5432 and a Transaction mode pooler on port 6543. Use Transaction mode (6543) for Apps Script since each script execution opens a short-lived connection.
Neon
Neon's connection string:
postgres://alice:[email protected]/mydb?sslmode=require
Convert to JDBC:
jdbc:postgresql://ep-cool-name-123456.us-east-2.aws.neon.tech:5432/mydb?user=alice&password=password&ssl=true
Cold start behavior: Neon databases on the free tier scale to zero after inactivity. The first connection after a cold start can take 3–5 seconds. This is usually not a problem for Apps Script triggers but will appear in execution logs as a slow connection establishment. If your trigger runs frequently enough (every few hours), the database stays warm.
// Neon: standard port 5432, standard ssl=true // Key: DB_URL // Value: jdbc:postgresql://ep-your-endpoint.us-east-2.aws.neon.tech:5432/mydb?user=alice&password=pass&ssl=true
Heroku Postgres
Heroku provides a DATABASE_URL environment variable in this format:
postgres://user:[email protected]:5432/dbname
Convert to JDBC:
jdbc:postgresql://ec2-XX-XXX-XXX-XXX.compute-1.amazonaws.com:5432/dbname?user=user&password=password&ssl=true
Heroku-specific: Heroku Postgres requires SSL. The ssl=true parameter is mandatory: connections without it will be rejected. Heroku uses self-signed certificates on some plans; if you get SSL verification errors, add sslfactory=org.postgresql.ssl.NonValidatingFactory as a query parameter, though this disables certificate verification.
// Heroku: ssl=true is mandatory // Key: DB_URL // Value: jdbc:postgresql://ec2-1-2-3-4.compute-1.amazonaws.com:5432/mydb?user=u&password=p&ssl=true
Render
Render's internal database URL:
postgresql://mydb_user:[email protected]/mydb
Convert to JDBC:
jdbc:postgresql://dpg-XXXX-a.oregon-postgres.render.com:5432/mydb?user=mydb_user&password=password&ssl=true
Render-specific: Render databases are only accessible from within Render's network by default. To connect from Apps Script, you need to use the External Database URL (visible in your Render dashboard under your database's Info tab). The external hostname is different from the internal one. External connections require SSL, which ssl=true handles.
// Render: use External Database URL from Render dashboard, not internal // Key: DB_URL // Value: jdbc:postgresql://dpg-XXXX-a.oregon-postgres.render.com:5432/mydb?user=user&password=pass&ssl=true
Railway
Railway provides a DATABASE_URL variable:
postgresql://postgres:[email protected]:PORT/railway
Convert to JDBC:
jdbc:postgresql://roundhouse.proxy.rlwy.net:PORT/railway?user=postgres&password=password&ssl=true
Railway-specific: Railway uses a non-standard port (a randomly assigned high port, not 5432). The port is visible in your Railway dashboard and in the DATABASE_URL variable. Use whatever port Railway assigned: do not substitute 5432.
// Railway: use the exact port from your DATABASE_URL - it is NOT 5432 // Key: DB_URL // Value: jdbc:postgresql://roundhouse.proxy.rlwy.net:12345/railway?user=postgres&password=pass&ssl=true
Google Cloud SQL (PostgreSQL)
For Cloud SQL PostgreSQL, use Jdbc.getConnection() with the public IP and whitelist Google's IP ranges, or use the Cloud SQL proxy. The Jdbc.getCloudSqlConnection() shortcut is MySQL-only.
Your Cloud SQL public IP connection string:
jdbc:postgresql://YOUR_CLOUD_SQL_PUBLIC_IP:5432/mydb?user=postgres&password=password&ssl=true
Enable SSL on your Cloud SQL instance (recommended: require SSL in the Cloud SQL console). When SSL is required at the database level, ssl=true in the connection string is sufficient for Apps Script to negotiate the connection.
For Cloud SQL, you do not need to whitelist the full Google IP range if you use the Cloud SQL Auth Proxy. However, the proxy requires a server to host it: defeating the serverless appeal. For Apps Script, the simpler path is a public IP with the IP allowlist.
AWS RDS PostgreSQL
Your RDS connection string:
jdbc:postgresql://myinstance.XXXXXXXXXXXX.us-east-1.rds.amazonaws.com:5432/mydb?user=postgres&password=password&ssl=true
RDS-specific requirements:
- Your RDS instance must have Public accessibility set to
Yesin the AWS console (Modify > Connectivity > Public access). - Your VPC security group must allow inbound TCP on port 5432 from Google's IP ranges (
https://www.gstatic.com/ipranges/goog.txt). This is 60+ CIDR ranges. See the MySQL guide for a discussion of the security tradeoffs. - RDS PostgreSQL supports SSL. The
ssl=trueparameter works. For certificate validation, download the RDS CA bundle and configuresslrootcert, though most teams skip this for internal tools.
// RDS: Public accessibility on, security group open to Google IPs // Key: DB_URL // Value: jdbc:postgresql://mydb.xxxx.us-east-1.rds.amazonaws.com:5432/mydb?user=postgres&password=pass&ssl=true
JSONB and UUID: The Type Casting Problem
This is the most subtle failure mode in the PostgreSQL + Apps Script stack, and it is completely undocumented outside of Justin Poehnelt's February 2026 post.
Apps Script's JDBC driver is a Java-based driver that maps PostgreSQL types to Java types. It handles standard types, VARCHAR, INTEGER, BOOLEAN, TIMESTAMP, NUMERIC, correctly. It fails on PostgreSQL-specific types:
- **
UUID** columns return a JDBC type that Apps Script cannot serialize to a string. - **
JSONB** columns return binary data that appears as garbage or throws a cast error. - **
JSON** columns have the same problem as JSONB. - **
ARRAYtypes** (text[],integer[]) are similarly problematic.
**The fix: cast to ::text in your SQL query.**
-- This fails silently or errors: SELECT id, metadata, tags FROM products; -- This works: SELECT id::text, metadata::text, tags::text FROM products;
In Apps Script code:
function queryWithTypeCasting() {
const conn = getDbConnection();
try {
const stmt = conn.createStatement();
const results = stmt.executeQuery(`
SELECT
id::text AS id,
user_id::text AS user_id,
payload::text AS payload,
created_at::text AS created_at,
status
FROM events
ORDER BY created_at DESC
LIMIT 1000
`);
const meta = results.getMetaData();
const numCols = meta.getColumnCount();
const rows = [];
// Headers
const headers = [];
for (let i = 1; i <= numCols; i++) {
headers.push(meta.getColumnName(i));
}
rows.push(headers);
// Data
while (results.next()) {
const row = [];
for (let i = 1; i <= numCols; i++) {
row.push(results.getString(i));
}
rows.push(row);
}
results.close();
stmt.close();
const sheet = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('Events') ||
SpreadsheetApp.getActiveSpreadsheet().insertSheet('Events');
sheet.clearContents();
sheet.getRange(1, 1, rows.length, numCols).setValues(rows);
} finally {
conn.close();
}
}The ::text cast tells PostgreSQL to serialize the value as a text string before handing it to the JDBC driver. The JDBC driver can always handle text. Your JSONB data arrives as a JSON string in the cell, which is usually what you want for a spreadsheet view anyway.
UUID gotcha: UUID columns returned without a cast produce a java.util.UUID object that Apps Script sometimes renders as an empty string. Cast UUIDs to text:
SELECT id::text, name, created_at FROM users;
ARRAY gotcha: PostgreSQL arrays do not have a clean JDBC mapping. Cast them:
SELECT tags::text FROM posts; -- Returns '{tag1,tag2,tag3}' as a stringIf you need the array as a comma-separated list instead:
SELECT array_to_string(tags, ', ') AS tags FROM posts;
SSL and TLS Configuration
Apps Script requires TLS 1.2 or higher for all JDBC connections. TLS 1.0 and 1.1 are disabled on the Apps Script side, even if your database accepts older TLS versions, Apps Script will refuse to negotiate them.
For most managed PostgreSQL providers (Supabase, Neon, Heroku, Render, Railway, Cloud SQL, RDS), TLS 1.2+ is already enforced at the database level. You are unlikely to encounter TLS version issues with these providers.
**The ssl=true parameter** tells the JDBC driver to negotiate an SSL/TLS encrypted connection. It is the equivalent of sslmode=require in the modern URI format, with one important difference: it does not validate the server certificate by default. This means the connection is encrypted but not verified against a certificate authority.
For internal tools and dashboards, this is acceptable for most organizations. For applications where certificate validation is a compliance requirement:
jdbc:postgresql://host:5432/db?user=u&password=p&ssl=true&sslfactory=org.postgresql.ssl.jdbc4.LibPQFactory&sslmode=verify-ca
However, LibPQFactory requires the CA certificate to be available in the Java truststore that Apps Script uses, which is not configurable from the script level. In practice, ssl=true without certificate validation is the maximum achievable SSL configuration from Apps Script without significant workarounds.
Provider SSL notes:
| Provider | SSL Required? | Recommended SSL param |
|---|---|---|
| Supabase | Yes | ssl=true |
| Neon | Yes | ssl=true |
| Heroku | Yes | ssl=true |
| Render | Yes (external) | ssl=true |
| Railway | Yes | ssl=true |
| Cloud SQL | Configurable | ssl=true |
| RDS | Configurable | ssl=true |
| Local/private | No | Omit or ssl=false |
Auto-Refresh: Setting Up Time-Driven Triggers
Once your connection works, wrap it in a time-driven trigger to auto-refresh your sheet:
/**
* Main refresh function - pulls data from PostgreSQL into a Sheet.
* Trigger this on a schedule.
*/
function refreshPostgresData() {
const conn = getDbConnection();
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('Live Data') ||
ss.insertSheet('Live Data');
try {
const stmt = conn.createStatement();
stmt.setMaxRows(10000);
const results = stmt.executeQuery(`
SELECT
id::text,
customer_name,
amount::text,
metadata::text,
created_at::text
FROM orders
WHERE created_at >= NOW() - INTERVAL '7 days'
ORDER BY created_at DESC
`);
const meta = results.getMetaData();
const numCols = meta.getColumnCount();
const rows = [];
const headers = [];
for (let i = 1; i <= numCols; i++) {
headers.push(meta.getColumnName(i));
}
rows.push(headers);
while (results.next()) {
const row = [];
for (let i = 1; i <= numCols; i++) {
row.push(results.getString(i));
}
rows.push(row);
}
results.close();
stmt.close();
sheet.clearContents();
if (rows.length > 0) {
sheet.getRange(1, 1, rows.length, numCols).setValues(rows);
}
// Timestamp so users know when data was last pulled
const meta2 = ss.getSheetByName('_meta') || ss.insertSheet('_meta');
meta2.getRange('A1').setValue('Last refreshed');
meta2.getRange('B1').setValue(new Date());
} catch (e) {
MailApp.sendEmail({
to: Session.getActiveUser().getEmail(),
subject: '[Sheets] PostgreSQL sync failed',
body: 'Error: ' + e.message
});
throw e;
} finally {
conn.close();
}
}
/**
* Run this once to install the hourly trigger.
*/
function installHourlyTrigger() {
// Remove existing triggers for this function
ScriptApp.getProjectTriggers()
.filter(t => t.getHandlerFunction() === 'refreshPostgresData')
.forEach(t => ScriptApp.deleteTrigger(t));
ScriptApp.newTrigger('refreshPostgresData')
.timeBased()
.everyHours(1)
.create();
Logger.log('Hourly trigger installed.');
}Run installHourlyTrigger() once. Your sheet will refresh every hour, alert you on failure via email, and write a "Last refreshed" timestamp for visibility.
Comparison Table: Providers and Their Quirks
| Provider | Default Port | Use Pooler? | Pooler Port | SSL Required | Special Notes |
|---|---|---|---|---|---|
| Supabase | 5432 | Yes | 6543 | Yes | Username includes project ref ID |
| Neon | 5432 | No | - | Yes | Cold start on free tier |
| Heroku | 5432 | No | - | Yes | Use ssl=true; cert may be self-signed |
| Render | 5432 | No | - | Yes (external) | Use External Database URL |
| Railway | Custom | No | - | Yes | Port is random, check dashboard |
| Cloud SQL PG | 5432 | No | - | Configurable | Whitelist Google IPs or use Auth Proxy |
| AWS RDS PG | 5432 | No | - | Configurable | Public accessibility + security group rules |
| Local/Docker | 5432 | No | - | No | Not reachable from Apps Script |
Troubleshooting
"Failed to establish a database connection. Check connection string, username and password."
This error covers almost everything. Work through this in order:
- Confirm the protocol is
jdbc:postgresql://, notpostgres://, notpostgresql://, notjdbc:postgres://. - Confirm the port is explicit in the URL (
:5432or the custom port). - Confirm credentials are in query parameters (
?user=x&password=y), not in the URL path. - Run
testConnection()and check the Apps Script execution log for the exact error. - Confirm the database host is publicly reachable (not behind a VPN or private subnet only).
- For Supabase: confirm you are using port 6543 and the pooler hostname.
- For Railway: confirm you are using the exact port from your dashboard, not 5432.
"JSONB column returns empty or errors"
Cast to text in your query: SELECT payload::text FROM table. See the type casting section.
"UUID column is empty in the sheet"
Cast to text: SELECT id::text FROM table. UUID is a PostgreSQL-specific type that the JDBC driver cannot serialize without the cast.
"Connection works manually but fails on trigger"
The trigger runs from Google's servers, which have different outbound IPs than your browser session. Your database firewall may be blocking the trigger's IP. For Render and Heroku, external connections are allowed by default. For RDS and Cloud SQL, check your security group / authorized networks for the full Google IP list at https://www.gstatic.com/ipranges/goog.txt.
"Neon connection times out occasionally"
Neon scales to zero on the free tier. Cold starts cause the first connection to take longer than Apps Script's connection timeout in some configurations. Upgrade to a paid Neon plan (which keeps the database warm) or use a trigger frequency that keeps the database active.
"TLS handshake failed"
Your database may be configured to require sslmode=verify-full, which validates the server certificate. Apps Script's JDBC driver does not support full certificate verification without access to a configurable truststore. Change your database SSL requirement to sslmode=require (encrypted, certificate not verified) to match what ssl=true provides.
"Too many connections" on Supabase
You are using the direct connection (port 5432) instead of the pooler (port 6543). Each Apps Script execution opens a new connection and does not reuse it. On a busy trigger schedule, this exhausts Supabase's direct connection limit quickly. Switch to the pooler URL.
The Bottom Line
PostgreSQL support in Google Apps Script JDBC is the most significant addition to the Apps Script data connectivity story in years. It removes the middleware requirement for teams whose database is already PostgreSQL, which in 2026 means most SaaS products, most cloud-native applications, and most companies using Supabase, Neon, Heroku, or a managed cloud database.
The learning curve is front-loaded: the connection string format mismatch is a blocker until you understand it, and the JSONB/UUID type casting requirement is a silent failure mode. Both are documented here now.
Once the connection works, the pattern is solid. A 50-line Apps Script with an hourly trigger gives you a live PostgreSQL view in any Google Sheet, for free, with no servers to maintain. For most internal dashboards, reporting views, and operations tools, that is the right answer.
The constraints remain the same as with MySQL: 60+ IP ranges to whitelist, a 6-minute execution ceiling, and unencrypted credential storage in Script Properties. For teams where those constraints are dealbreakers: security policies that prohibit broad IP whitelists, high-refresh-frequency requirements, or multi-sheet synchronization needs: a managed connector like brooked.io handles the infrastructure layer and supports PostgreSQL with the same no-code setup, static IPs, and encrypted credentials that it provides for MySQL. But for teams comfortable with a bit of script maintenance, the native JDBC path is now fully viable.
Troubleshooting quick reference
Frequently asked questions
Is PostgreSQL support in Apps Script JDBC officially supported by Google?
Yes. As of 2026, the Apps Script JDBC documentation lists PostgreSQL as a supported database type alongside MySQL, Microsoft SQL Server, and Oracle. Justin Poehnelt, a Senior Software Engineer at Google who works on Apps Script, published a detailed guide confirming the feature and its behavior on February 17, 2026.
Can I use the modern postgres:// URI format at all?
No. Jdbc.getConnection() requires the jdbc:postgresql:// format. This is a fundamental constraint of the Java-based JDBC driver that Apps Script uses. There is no configuration option to accept the modern URI format. You must convert the string before using it in Apps Script.
Does Apps Script support PostgreSQL 16 / 17 features?
Apps Script's JDBC driver communicates with the PostgreSQL wire protocol, which is backward-compatible. Modern PostgreSQL versions (15, 16, 17) work fine for standard queries. Feature-specific syntax (new aggregate functions, JSON operators introduced in PG16, etc.) works at the SQL level. The JDBC layer only affects how results are returned to Apps Script, which is where the ::text cast requirement for JSONB and UUID applies, regardless of PostgreSQL version.
What about prepared statements and parameterized queries?
Fully supported. Use conn.prepareStatement(): This is the correct approach for any query that includes user-controlled values. It prevents SQL injection and works correctly with all standard PostgreSQL types.
Can I write data back from Google Sheets to PostgreSQL?
Yes. Use INSERT, UPDATE, or DELETE statements with prepareStatement(). Ensure the database user in your connection string has write permissions on the target tables. Two-way sync (detecting which cells changed and writing only those back) requires additional logic in Apps Script to track changes, and carries the risk of race conditions if multiple users edit the sheet simultaneously.
Ready to connect your data to Google Sheets?
Brooked's free tier covers 100 imports per month with AI Analyst included, no credit card required.
Install Brooked free →

