|
| 1 | +# PostgreSQL Wire Protocol (PGWire) Compatibility |
| 2 | + |
| 3 | +*SpacetimeDB* supports the [PostgreSQL wire protocol (](https://www.postgresql.org/docs/current/protocol.html)[ |
| 4 | +*PGWire*](https://www.postgresql.org/docs/current/protocol.html)[)](https://www.postgresql.org/docs/current/protocol.html), |
| 5 | +enabling compatibility with PostgreSQL clients and tools. |
| 6 | + |
| 7 | +The PostgreSQL wire protocol is a network protocol used by PostgreSQL clients to communicate with compatible servers. It |
| 8 | +defines how messages are formatted and exchanged between client and server. The protocol is agnostic to the query |
| 9 | +dialect, |
| 10 | +meaning it can be used with different SQL engines and feature sets, in concrete, *SpacetimeDB*. |
| 11 | + |
| 12 | +This allows users to leverage the existing PostgreSQL ecosystem, including drivers, ORMs, IDEs, CLI tools, and GUI |
| 13 | +clients that support PostgreSQL. |
| 14 | + |
| 15 | +When using *PGWire* with *SpacetimeDB*, consider the following: |
| 16 | + |
| 17 | +## Feature Support |
| 18 | + |
| 19 | +SpacetimeDB is progressively adding PostgreSQL client compatibility. Some features are unsupported, partially |
| 20 | +implemented, or behave differently: |
| 21 | + |
| 22 | +- **Protocol Version**: Only *PGWire* protocol *version 3.0* is supported, and only the *Simple Query Protocol*, and |
| 23 | + without parameterized queries. |
| 24 | +- **SQL Features**: Only the subset of SQL features documented in the [SQL documentation](index.md) are |
| 25 | + supported. Subscription queries do not update in real time. |
| 26 | +- **Authentication**: SpacetimeDB does not implement database users or roles. The connection string |
| 27 | + `user_name@database_name` ignores `user_name`; only `database_name` is used. Authentication is based on the *auth |
| 28 | + token* |
| 29 | + provided via the `password` field. |
| 30 | +- **SSL/TLS**: SSL is supported only for `maincloud` deployments (without mutual TLS). Other deployments (such as |
| 31 | + `standalone`) do not support SSL/TLS connections. |
| 32 | +- **System Tables and Views**: SpacetimeDB provides its own system tables (e.g., `SELECT * FROM st_table`) for |
| 33 | + introspection. These are not PostgreSQL-compatible, so tools relying on PostgreSQL system catalogs will not work. |
| 34 | +- **Port and Host**: |
| 35 | + - In `standalone` deployments, specify the port with `spacetime start --pg-port <port>`. Without this flag, |
| 36 | + connections using the PostgreSQL protocol are not enabled. |
| 37 | + - In `maincloud` deployments, the port is always `5432`. |
| 38 | +- **Transactions**: User-defined transactions (`BEGIN TRANSACTION`, `COMMIT`, etc.) are not supported. Each SQL |
| 39 | + statement executes in its own transaction context. Client libraries should disable automatic transaction handling. |
| 40 | + |
| 41 | +## Connection Parameters |
| 42 | + |
| 43 | +To connect to SpacetimeDB using a PostgreSQL client, use the following parameters: |
| 44 | + |
| 45 | +- **Host**: |
| 46 | + - `localhost` for `standalone` or `local` deployments |
| 47 | + - `maincloud.spacetimedb.com` for `maincloud` deployments |
| 48 | +- **Port**: |
| 49 | + - `5432` for `maincloud` |
| 50 | + - The value passed with `--pg-port` for `standalone` |
| 51 | +- **Database**: The target SpacetimeDB database |
| 52 | +- **User**: Any string (ignored by SpacetimeDB) |
| 53 | +- **Password**: The `auth token` |
| 54 | +- **SSL Mode**: `require` (only for `maincloud`) |
| 55 | + |
| 56 | +### Auth Token |
| 57 | + |
| 58 | +> **Warning**: The `auth token` is sensitive. Do not expose it in logs, version control, or insecure locations. |
| 59 | +
|
| 60 | +SpacetimeDB uses the `password` field to pass the `auth token`. Obtain the token with: |
| 61 | + |
| 62 | +```bash |
| 63 | +spacetime login show --token |
| 64 | +``` |
| 65 | + |
| 66 | +To export the token to `PGPASSWORD`: |
| 67 | + |
| 68 | +```python |
| 69 | +# token.py |
| 70 | +import sys, re |
| 71 | + |
| 72 | +text = sys.stdin.read() |
| 73 | +match = re.search(r"Your auth token \(don't share this!\) is\s+(\S+)", text) |
| 74 | +if not match: |
| 75 | + sys.exit("No token found") |
| 76 | + |
| 77 | +print(f"export PGPASSWORD={match.group(1)}") |
| 78 | +``` |
| 79 | + |
| 80 | +```bash |
| 81 | +eval "$(spacetime login show --token | python3 ~/token.py)" |
| 82 | +``` |
| 83 | + |
| 84 | +## Examples |
| 85 | + |
| 86 | +Assume you are using the `quickstart-chat` database created in |
| 87 | +the [Rust Module Quickstart](/docs/modules/rust/quickstart) or [C# Module Quickstart](/docs/modules/c-sharp/quickstart), |
| 88 | +and have set the `auth token` as shown above. |
| 89 | + |
| 90 | +### Using `psql` |
| 91 | + |
| 92 | +Standalone or local deployment: |
| 93 | + |
| 94 | +```bash |
| 95 | +psql "host=localhost port=5432 user=any dbname=quickstart-chat" |
| 96 | +``` |
| 97 | + |
| 98 | +Maincloud deployment: |
| 99 | + |
| 100 | +```bash |
| 101 | +psql "host=maincloud.spacetimedb.com port=5432 user=any dbname=quickstart-chat sslmode=require" |
| 102 | +``` |
| 103 | + |
| 104 | +> **Note**: Introspection commands such as `\dt` will not work, as SpacetimeDB does not support PostgreSQL schemas. |
| 105 | +
|
| 106 | +### Using Python (`psycopg2`) |
| 107 | + |
| 108 | +```python |
| 109 | +import psycopg2 |
| 110 | +import os |
| 111 | + |
| 112 | +conn = psycopg2.connect( |
| 113 | + host="localhost", # or "maincloud.spacetimedb.com" for maincloud |
| 114 | + port=5432, |
| 115 | + dbname="quickstart-chat", |
| 116 | + user="any", |
| 117 | + password=os.getenv("PGPASSWORD"), |
| 118 | + sslmode="disable" # use "require" for maincloud |
| 119 | +) |
| 120 | +conn.set_session(autocommit=True) # disable transactions |
| 121 | + |
| 122 | +print("Running query:") |
| 123 | +with conn.cursor() as cur: |
| 124 | + cur.execute("SELECT * FROM message;") |
| 125 | + for row in cur.fetchall(): |
| 126 | + print(row) |
| 127 | + |
| 128 | +conn.close() |
| 129 | +print("Done.") |
| 130 | +``` |
| 131 | + |
| 132 | +### Using Rust (`tokio-postgres` + `rustls`) |
| 133 | + |
| 134 | +We use the `tokio-postgres-rustls` because is stricter, so we can show how disables certificate verification. |
| 135 | + |
| 136 | +```toml |
| 137 | +# Cargo.toml |
| 138 | +[dependencies] |
| 139 | +anyhow = "1.0.71" |
| 140 | +tokio-postgres = "0.7.14" |
| 141 | +tokio-postgres-rustls = "0.13.0" |
| 142 | +tokio = { version = "1.47.1", features = ["full"] } |
| 143 | +rustls = "0.23.32" |
| 144 | +``` |
| 145 | + |
| 146 | +```rust |
| 147 | +// main.rs |
| 148 | +use std::env; |
| 149 | +use std::sync::Arc; |
| 150 | +use rustls::client::danger::{ServerCertVerified, ServerCertVerifier}; |
| 151 | +use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; |
| 152 | +use rustls::{ClientConfig, Error, RootCertStore, SignatureScheme}; |
| 153 | +use tokio_postgres_rustls::MakeRustlsConnect; |
| 154 | + |
| 155 | +#[derive(Debug)] |
| 156 | +struct NoVerifier; |
| 157 | + |
| 158 | +impl ServerCertVerifier for NoVerifier { |
| 159 | + fn verify_server_cert( |
| 160 | + &self, |
| 161 | + _: &CertificateDer<'_>, |
| 162 | + _: &[CertificateDer<'_>], |
| 163 | + _: &ServerName<'_>, |
| 164 | + _: &[u8], |
| 165 | + _: UnixTime, |
| 166 | + ) -> Result<ServerCertVerified, Error> { |
| 167 | + Ok(ServerCertVerified::assertion()) |
| 168 | + } |
| 169 | + |
| 170 | + fn verify_tls12_signature( |
| 171 | + &self, |
| 172 | + _: &[u8], |
| 173 | + _: &CertificateDer<'_>, |
| 174 | + _: &rustls::DigitallySignedStruct, |
| 175 | + ) -> Result<rustls::client::danger::HandshakeSignatureValid, Error> { |
| 176 | + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) |
| 177 | + } |
| 178 | + |
| 179 | + fn verify_tls13_signature( |
| 180 | + &self, |
| 181 | + _: &[u8], |
| 182 | + _: &CertificateDer<'_>, |
| 183 | + _: &rustls::DigitallySignedStruct, |
| 184 | + ) -> Result<rustls::client::danger::HandshakeSignatureValid, Error> { |
| 185 | + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) |
| 186 | + } |
| 187 | + |
| 188 | + fn supported_verify_schemes(&self) -> Vec<SignatureScheme> { |
| 189 | + vec![ |
| 190 | + SignatureScheme::ECDSA_NISTP384_SHA384, |
| 191 | + SignatureScheme::ECDSA_NISTP256_SHA256, |
| 192 | + SignatureScheme::RSA_PSS_SHA512, |
| 193 | + SignatureScheme::RSA_PSS_SHA384, |
| 194 | + SignatureScheme::RSA_PSS_SHA256, |
| 195 | + SignatureScheme::ED25519, |
| 196 | + ] |
| 197 | + } |
| 198 | +} |
| 199 | + |
| 200 | +#[tokio::main] |
| 201 | +async fn main() -> Result<(), anyhow::Error> { |
| 202 | + let password = env::var("PGPASSWORD").expect("PGPASSWORD not set"); |
| 203 | + |
| 204 | + let mut config = ClientConfig::builder() |
| 205 | + .with_root_certificates(RootCertStore::empty()) |
| 206 | + .with_no_client_auth(); |
| 207 | + |
| 208 | + config.dangerous().set_certificate_verifier(Arc::new(NoVerifier)); |
| 209 | + let connector = MakeRustlsConnect::new(config); |
| 210 | + |
| 211 | + let (client, connection) = tokio_postgres::connect( |
| 212 | + &format!( |
| 213 | + "host=localhost port=5432 user=any sslmode=require dbname=quickstart-chat password={password}" |
| 214 | + ), |
| 215 | + connector, |
| 216 | + ).await?; |
| 217 | + |
| 218 | + tokio::spawn(async move { connection.await.expect("connection error") }); |
| 219 | + |
| 220 | + println!("Running query:"); |
| 221 | + let rows = client.simple_query("SELECT * FROM message;").await?; |
| 222 | + for row in rows { |
| 223 | + println!("Row: {:?}", row); |
| 224 | + } |
| 225 | + println!("Done."); |
| 226 | + Ok(()) |
| 227 | +} |
| 228 | +``` |
| 229 | + |
0 commit comments