Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions .github/workflows/deploy-stripe-worker.yml

This file was deleted.

2 changes: 0 additions & 2 deletions docs/authentication/google.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
title: Google
---

<Info>This feature requires the **Team** or **Enterprise** plan.</Info>

Allow users to sign in with their Google accounts.

## Prerequisites
Expand Down
2 changes: 0 additions & 2 deletions docs/authentication/keycloak.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
title: Keycloak
---

<Info>This feature requires the **Enterprise** plan.</Info>

Allow users to sign in with Keycloak, an open source identity and access management solution.

## Prerequisites
Expand Down
2 changes: 0 additions & 2 deletions docs/authentication/okta.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
title: Okta
---

<Info>This feature requires the **Enterprise** plan.</Info>

Allow users to sign in with Okta, a cloud identity and access management platform.

## Prerequisites
Expand Down
2 changes: 1 addition & 1 deletion docs/authentication/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pgconsole supports multiple authentication methods to secure access to your data
| Method | Description | Best For |
|--------|-------------|----------|
| Email/Password | Built-in local authentication | Simple setups, small teams |
| SSO ([Google](/authentication/google), [Okta](/authentication/okta), [Keycloak](/authentication/keycloak)) | OAuth providers | Enterprise, existing identity providers |
| SSO ([Google](/authentication/google), [Okta](/authentication/okta), [Keycloak](/authentication/keycloak)) | OAuth providers | Organizations with existing identity providers |

## Configuration

Expand Down
13 changes: 5 additions & 8 deletions docs/configuration/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ pgconsole uses a TOML configuration file for all settings. Pass the config file
```toml pgconsole.toml
[general]
external_url = "https://pgconsole.example.com"
license = "your-license-key"

[general.banner]
text = "System maintenance scheduled for Sunday 2am UTC"
Expand Down Expand Up @@ -142,12 +141,10 @@ permissions = ["read"]
| Field | Description | Required |
|-------|-------------|----------|
| `external_url` | Public URL of the application. See [Configure External Access](/configuration/external-access). | Required for SSO |
| `license` | License key. See [Manage License](/configuration/license). | |

```toml pgconsole.toml
[general]
external_url = "https://pgconsole.example.com"
license = "your-license-key"
```

### Announcement Banner
Expand All @@ -169,7 +166,7 @@ color = "#7c3aed"

## Branding

Replace the pgconsole logo with your own. Requires an [Enterprise license](/configuration/license).
Replace the pgconsole logo with your own.

| Field | Description | Required |
|-------|-------------|----------|
Expand Down Expand Up @@ -307,7 +304,7 @@ User entries. Repeat for multiple users. Users with a `password` can sign in wit
|-------|-------------|----------|
| `email` | User email or identifier | Yes |
| `password` | Login password (omit for SSO-only users) | |
| `owner` | Grants access to subscription management | |
| `owner` | Marks the user as an owner (shown with an Owner badge) | |

```toml pgconsole.toml
[[users]]
Expand All @@ -322,7 +319,7 @@ email = "alice@example.com"

### Owner Role

Users with `owner = true` can view subscription status and access upgrade options. See [Manage License](/configuration/license). If no user has `owner = true`, the first user entry automatically becomes the owner.
Users with `owner = true` are marked with an Owner badge in the UI. If no user has `owner = true`, the first user entry automatically becomes the owner.

## Groups

Expand All @@ -345,7 +342,7 @@ members = ["admin@example.com", "alice@example.com"]

## Access Control (IAM)

Rules for controlling access to connections. See [Database Access Control](/features/database-access-control) for a full guide on permissions, patterns, and examples.
Rules for controlling access to connections. IAM is opt-in: with no `[[iam]]` rules defined, all authenticated users have full access, and enforcement begins once you define the first rule. See [Database Access Control](/features/database-access-control) for a full guide on permissions, patterns, and examples.

| Field | Description | Required |
|-------|-------------|----------|
Expand Down Expand Up @@ -410,7 +407,7 @@ base_url = "https://api.groq.com/openai/v1"

## Agents

Non-human principals that authenticate to the [MCP Server](/features/mcp-server) with a bearer token. An agent is **not** a [user](#users) — it can't log into the UI and doesn't count against your license seats. Repeat for multiple agents.
Non-human principals that authenticate to the [MCP Server](/features/mcp-server) with a bearer token. An agent is **not** a [user](#users) — it can't log into the UI. Repeat for multiple agents.

| Field | Description | Required |
|-------|-------------|----------|
Expand Down
29 changes: 0 additions & 29 deletions docs/configuration/license.mdx

This file was deleted.

1 change: 0 additions & 1 deletion docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"pages": [
"configuration/server-flags",
"configuration/external-access",
"configuration/license",
"configuration/config"
]
}
Expand Down
2 changes: 0 additions & 2 deletions docs/features/audit-log.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
title: Audit Log
---

<Info>This feature requires the **Enterprise** plan.</Info>

pgconsole emits audit logs as JSON lines to stdout, allowing you to capture and process them with your existing log infrastructure.

## Events
Expand Down
10 changes: 5 additions & 5 deletions docs/features/database-access-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
title: Database Access Control
---

<Info>This feature requires the **Team** or **Enterprise** plan.</Info>

pgconsole provides fine-grained access control for your database connections. You define [IAM rules](/configuration/config#access-control-iam) in your configuration file, and pgconsole enforces them — rejecting unauthorized queries before they reach the database.

<Info>IAM is **opt-in**. With no `[[iam]]` rules defined, every authenticated user has full access to all connections. Enforcement begins the moment you define your first rule — from then on, any user without a matching rule is denied.</Info>

```mermaid
flowchart LR
U[User] -->|SQL| P[pgconsole]
Expand All @@ -19,17 +19,17 @@ Unlike PostgreSQL's built-in role system (`GRANT`/`REVOKE`), pgconsole's access

![IAM Permission Denied](/images/features/database-access-control/iam-permission-denied.webp)

Access control in pgconsole works on three principles:
Once at least one rule is defined, access control works on three principles:

- **Default deny** — users have no access unless a rule explicitly grants it
- **Connection-scoped** — permissions are granted per database connection, not globally
- **Disjoint permissions** — each permission level is independent; `write` does not imply `read`

When IAM is enabled, users only see connections they have at least one permission for. Connections without any matching rules are hidden entirely.
While IAM is active, users only see connections they have at least one permission for. Connections without any matching rules are hidden entirely.

## Prerequisites

- [Authentication](/configuration/config#authentication) must be enabled; otherwise, all users get full access to all connections
- [Authentication](/configuration/config#authentication) must be enabled, and at least one `[[iam]]` rule must be defined; otherwise, all users get full access to all connections
- Users must be defined in [`[[users]]`](/configuration/config#users)
- Groups (if used) must be defined in [`[[groups]]`](/configuration/config#groups)

Expand Down
6 changes: 4 additions & 2 deletions docs/features/mcp-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ Requests without a valid token are rejected with `401`.

## Identity

Each token belongs to an `[[agents]]` entry — a non-human principal that is **not** a user (no UI login, no license seat). There are two kinds:
Each token belongs to an `[[agents]]` entry — a non-human principal that is **not** a user (no UI login). There are two kinds:

- **Pure agent** — a standalone service account (e.g. a CI bot). Authorized by IAM rules whose `members` include `agent:<id>`. Audited as `agent:<id>`.
If no `[[iam]]` rules are defined, IAM is off and agents have full access to all connections; define at least one rule to enforce least privilege.

- **Pure agent** — a standalone service account (e.g. a CI bot). When IAM is active, authorized by IAM rules whose `members` include `agent:<id>`. Audited as `agent:<id>`.
Comment on lines +27 to +31
- **Delegated agent** — acts `on_behalf_of` a user, **inheriting that user's permissions** narrowed by optional `permissions`/`connections` caps. It can never exceed the user and loses access automatically when the user does. Audited as the user, tagged with the agent.
Comment thread
Copilot marked this conversation as resolved.

```toml pgconsole.toml
Expand Down
2 changes: 0 additions & 2 deletions docs/features/white-labeling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
title: White Labeling
---

<Info>This feature requires the **Enterprise** plan.</Info>

pgconsole supports white labeling, allowing you to rebrand and embed the console into your own product.

- **Custom Logo** — Replace the default logo with your own in the top-right corner.
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/faq.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ No. pgconsole is fully self-hosted and makes no outbound network requests except

## License

pgconsole is [source available](https://github.com/pgplex/pgconsole/blob/main/LICENSE). The Community plan is free to use. A commercial license is required for the Team or Enterprise plan.
pgconsole is [source available](https://github.com/pgplex/pgconsole/blob/main/LICENSE) and free to use, with all features included.

## Certifications

Expand Down
5 changes: 2 additions & 3 deletions pgconsole.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
# =============================================================================
# [general]
# external_url = "https://pgconsole.example.com" # Required when OAuth providers are enabled
# license = "eyJhbGciOiJSUzI1NiJ9..." # License key (JWT). Omit for FREE plan.
#
# # Banner (optional) - displays at the top of the page, cannot be dismissed
# [general.banner]
Expand All @@ -15,9 +14,9 @@
# color = "#7c3aed" # Optional

# =============================================================================
# Branding (Enterprise only)
# Branding
# =============================================================================
# Replace the pgconsole logo with your own. Requires an Enterprise license.
# Replace the pgconsole logo with your own.
#
# [branding]
# logo = "https://example.com/your-logo.svg"
Expand Down
42 changes: 3 additions & 39 deletions server/auth-routes.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { Router, type Request, type Response } from 'express'
import crypto from 'crypto'
import { createToken, verifyToken, authenticateBasic } from './lib/auth'
import { getAuthConfig, isAuthEnabled, getGroupsForUser, getPlan, isOwner, getUsers } from './lib/config'
import { getAuthConfig, isAuthEnabled, getGroupsForUser, isOwner, getUsers } from './lib/config'
import { auditLogin, auditLogout } from './lib/audit'
import { registerGoogleOAuth } from './lib/oauth/google'
import { registerKeycloakOAuth } from './lib/oauth/keycloak'
import { registerOktaOAuth } from './lib/oauth/okta'
import { feature, requiredPlan } from '../src/lib/plan'
import type { Feature } from '../src/lib/plan'

const SSO_FEATURE: Record<string, Feature> = {
google: 'SSO_GOOGLE',
keycloak: 'SSO_KEYCLOAK',
okta: 'SSO_OKTA',
}

const router = Router()

Expand Down Expand Up @@ -123,28 +115,6 @@ const oauthOpts = {
generateState,
getClientIp,
}
// Gate OAuth routes by plan
router.use(['/google', '/google/callback'], (req: Request, res: Response, next) => {
if (!feature('SSO_GOOGLE', getPlan())) {
return res.status(403).json({ error: 'Google SSO requires Team plan or higher' })
}
next()
})

router.use(['/keycloak', '/keycloak/callback'], (req: Request, res: Response, next) => {
if (!feature('SSO_KEYCLOAK', getPlan())) {
return res.status(403).json({ error: 'Keycloak SSO requires Enterprise plan' })
}
next()
})

router.use(['/okta', '/okta/callback'], (req: Request, res: Response, next) => {
if (!feature('SSO_OKTA', getPlan())) {
return res.status(403).json({ error: 'Okta SSO requires Enterprise plan' })
}
next()
})

registerGoogleOAuth(router, oauthOpts)
registerKeycloakOAuth(router, oauthOpts)
registerOktaOAuth(router, oauthOpts)
Expand All @@ -156,16 +126,10 @@ router.get('/providers', (_req: Request, res: Response) => {
}

const config = getAuthConfig()
const plan = getPlan()
const providers: Array<{ name: string; requiredPlan?: string }> = []
const providers: Array<{ name: string }> = []
if (getUsers().some(u => u.password)) providers.push({ name: 'basic' })
for (const provider of config?.providers ?? []) {
const feat = SSO_FEATURE[provider.type]
if (feat && !feature(feat, plan)) {
providers.push({ name: provider.type, requiredPlan: requiredPlan(feat) })
} else {
providers.push({ name: provider.type })
}
providers.push({ name: provider.type })
}

return res.json({ providers })
Expand Down
28 changes: 3 additions & 25 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import cookieParser from 'cookie-parser'
import { authRouter } from './auth-routes'
import { connectRouter } from './connect'
import { mcpRouter, MCP_PATH } from './mcp'
import { loadConfig, loadConfigFromString, loadDemoConfig, isDemoMode, getBanner, getBranding, getExternalUrl, getPlan, getLicenseExpiry, getUsers, getLicenseMaxUsers, getLicenseEmail, getAgents } from './lib/config'
import { loadConfig, loadConfigFromString, loadDemoConfig, isDemoMode, getBanner, getBranding, getExternalUrl, getAgents } from './lib/config'
import { startDemoDatabase, stopDemoDatabase } from './lib/demo'
import { testAllConnections } from './lib/test-connections'
import { feature } from '../src/lib/plan'

// __dirname is provided by esbuild banner
declare const __dirname: string
Expand Down Expand Up @@ -40,15 +39,9 @@ app.use(connectRouter)

// Public settings endpoint (no auth required)
app.get('/api/setting', (_req, res) => {
const plan = getPlan()
res.json({
banner: feature('BANNER', plan) ? getBanner() : undefined,
branding: feature('BRANDING', plan) ? getBranding() : undefined,
plan,
licenseExpiry: getLicenseExpiry(),
licenseEmail: getLicenseEmail(),
maxUsers: getLicenseMaxUsers(),
userCount: getUsers().length,
banner: getBanner(),
branding: getBranding(),
demo: isDemoMode(),
})
Comment thread
tianzhou marked this conversation as resolved.
})
Expand Down Expand Up @@ -99,21 +92,6 @@ async function start() {
console.log(`✓ Demo database started on port ${demoPort}`)
}

// Check license expiration
const licenseExpiry = getLicenseExpiry()
if (licenseExpiry) {
const now = Math.floor(Date.now() / 1000)
if (licenseExpiry < now) {
console.warn('\n⚠️ WARNING: Your license has expired. Some features may be restricted.\n Renew at https://docs.pgconsole.com/configuration/license#purchasing-a-license\n')
}
}

// Log plan and user seat info
const plan = getPlan()
const maxUsers = getLicenseMaxUsers()
const userCount = getUsers().length
console.log(`✓ Plan: ${plan}, User seat: ${userCount}/${maxUsers}`)

// Test all connections to populate cache
try {
await testAllConnections()
Expand Down
Loading
Loading