From 2703cec52b775aa62e6837e774f126c599b73f01 Mon Sep 17 00:00:00 2001
From: tianzhou
Date: Thu, 25 Jun 2026 01:52:02 -0700
Subject: [PATCH 1/4] feat: remove pricing/licensing, make all features free
pgconsole is now entirely free with all features unconditionally
enabled. Removes the plan/license system end to end.
- Delete plan tiers, license verification, Subscription modal, license
expiry banner, the Stripe webhook worker, the website pricing page,
and the license docs page.
- Ungate features: SSO (Google/Keycloak/Okta), audit logging, groups,
banner, and branding are always available.
- IAM is now opt-in / off by default: with no [[iam]] rules defined,
authenticated users get full access; defining the first rule turns
enforcement on. Restores the pre-pricing default and eases onboarding.
- Scrub all pricing/plan/license references from docs.
- Update tests for the new IAM default; drop license/plan test files.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
docs/authentication/google.mdx | 2 -
docs/authentication/keycloak.mdx | 2 -
docs/authentication/okta.mdx | 2 -
docs/authentication/overview.mdx | 2 +-
docs/configuration/config.mdx | 13 +-
docs/configuration/license.mdx | 29 -
docs/docs.json | 1 -
docs/features/audit-log.mdx | 2 -
docs/features/database-access-control.mdx | 10 +-
docs/features/mcp-server.mdx | 2 +-
docs/features/white-labeling.mdx | 2 -
docs/getting-started/faq.mdx | 2 +-
server/auth-routes.ts | 42 +-
server/index.ts | 28 +-
server/lib/audit.ts | 4 -
server/lib/config.ts | 62 +-
server/lib/iam.ts | 18 +-
server/lib/license.ts | 46 -
src/App.tsx | 9 +-
src/components/AuthForm.tsx | 12 +-
src/components/Header.tsx | 10 +-
src/components/LicenseExpiryBanner.tsx | 47 -
src/components/SubscriptionModal.tsx | 177 --
src/hooks/useSetting.ts | 11 -
src/hooks/useSubscriptionModal.ts | 15 -
src/lib/auth-client.ts | 1 -
src/lib/plan.ts | 28 -
tests/iam.test.ts | 42 +-
tests/license.test.ts | 121 --
tests/mcp.test.ts | 22 +-
tests/plan.test.ts | 50 -
website/src/app/pricing/page.tsx | 115 -
website/src/app/sitemap.ts | 1 -
.../sections/plan-comparison-table.tsx | 137 --
.../sections/pricing-hero-multi-tier.tsx | 117 -
.../sections/pricing-multi-tier.tsx | 73 -
.../pricing-single-tier-two-column.tsx | 58 -
website/src/components/shared/navbar.tsx | 1 -
worker/stripe-webhook/package-lock.json | 1875 -----------------
worker/stripe-webhook/package.json | 14 -
worker/stripe-webhook/src/index.js | 360 ----
worker/stripe-webhook/wrangler.toml | 7 -
42 files changed, 57 insertions(+), 3515 deletions(-)
delete mode 100644 docs/configuration/license.mdx
delete mode 100644 server/lib/license.ts
delete mode 100644 src/components/LicenseExpiryBanner.tsx
delete mode 100644 src/components/SubscriptionModal.tsx
delete mode 100644 src/hooks/useSubscriptionModal.ts
delete mode 100644 src/lib/plan.ts
delete mode 100644 tests/license.test.ts
delete mode 100644 tests/plan.test.ts
delete mode 100644 website/src/app/pricing/page.tsx
delete mode 100644 website/src/components/sections/plan-comparison-table.tsx
delete mode 100644 website/src/components/sections/pricing-hero-multi-tier.tsx
delete mode 100644 website/src/components/sections/pricing-multi-tier.tsx
delete mode 100644 website/src/components/sections/pricing-single-tier-two-column.tsx
delete mode 100644 worker/stripe-webhook/package-lock.json
delete mode 100644 worker/stripe-webhook/package.json
delete mode 100644 worker/stripe-webhook/src/index.js
delete mode 100644 worker/stripe-webhook/wrangler.toml
diff --git a/docs/authentication/google.mdx b/docs/authentication/google.mdx
index 25d52bb..cd05895 100644
--- a/docs/authentication/google.mdx
+++ b/docs/authentication/google.mdx
@@ -2,8 +2,6 @@
title: Google
---
-This feature requires the **Team** or **Enterprise** plan.
-
Allow users to sign in with their Google accounts.
## Prerequisites
diff --git a/docs/authentication/keycloak.mdx b/docs/authentication/keycloak.mdx
index d1c91b2..0ce8430 100644
--- a/docs/authentication/keycloak.mdx
+++ b/docs/authentication/keycloak.mdx
@@ -2,8 +2,6 @@
title: Keycloak
---
-This feature requires the **Enterprise** plan.
-
Allow users to sign in with Keycloak, an open source identity and access management solution.
## Prerequisites
diff --git a/docs/authentication/okta.mdx b/docs/authentication/okta.mdx
index 8b17e94..47f4068 100644
--- a/docs/authentication/okta.mdx
+++ b/docs/authentication/okta.mdx
@@ -2,8 +2,6 @@
title: Okta
---
-This feature requires the **Enterprise** plan.
-
Allow users to sign in with Okta, a cloud identity and access management platform.
## Prerequisites
diff --git a/docs/authentication/overview.mdx b/docs/authentication/overview.mdx
index aeeb49d..4bc5a9c 100644
--- a/docs/authentication/overview.mdx
+++ b/docs/authentication/overview.mdx
@@ -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
diff --git a/docs/configuration/config.mdx b/docs/configuration/config.mdx
index 2b2b528..06b25b3 100644
--- a/docs/configuration/config.mdx
+++ b/docs/configuration/config.mdx
@@ -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"
@@ -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
@@ -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 |
|-------|-------------|----------|
@@ -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]]
@@ -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
@@ -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 |
|-------|-------------|----------|
@@ -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 |
|-------|-------------|----------|
diff --git a/docs/configuration/license.mdx b/docs/configuration/license.mdx
deleted file mode 100644
index 88a78e5..0000000
--- a/docs/configuration/license.mdx
+++ /dev/null
@@ -1,29 +0,0 @@
----
-title: Manage License
----
-
-pgconsole does not connect to any external license server, so it cannot auto-refresh the license. After purchasing a new subscription or renewing an existing one, a new license key will be emailed to you. Update the key in your configuration file and restart pgconsole for it to take effect.
-
-## Purchasing a License
-
-Purchase directly from the application via the **Subscription** option in the owner's profile menu, or use the links below:
-
-- [Monthly plan — $20/user/month](https://buy.stripe.com/aFa00ifoZ8hHfEDgeN1Fe02)
-- [Annual plan — $16/user/month](https://buy.stripe.com/28E28qfoZ69zcsr2nX1Fe03)
-
-A license key will be emailed to you after purchase.
-
-## Adding a License
-
-Add your license key to the configuration file:
-
-```toml pgconsole.toml
-[general]
-license = "your-license-key"
-```
-
-Restart pgconsole for the license to take effect.
-
-## Billing
-
-For billing issues, contact [billing@pgconsole.com](mailto:billing@pgconsole.com).
diff --git a/docs/docs.json b/docs/docs.json
index 37878d4..f164c90 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -52,7 +52,6 @@
"pages": [
"configuration/server-flags",
"configuration/external-access",
- "configuration/license",
"configuration/config"
]
}
diff --git a/docs/features/audit-log.mdx b/docs/features/audit-log.mdx
index 5a171b7..7e0d493 100644
--- a/docs/features/audit-log.mdx
+++ b/docs/features/audit-log.mdx
@@ -2,8 +2,6 @@
title: Audit Log
---
-This feature requires the **Enterprise** plan.
-
pgconsole emits audit logs as JSON lines to stdout, allowing you to capture and process them with your existing log infrastructure.
## Events
diff --git a/docs/features/database-access-control.mdx b/docs/features/database-access-control.mdx
index 2f4f3cd..4cbd677 100644
--- a/docs/features/database-access-control.mdx
+++ b/docs/features/database-access-control.mdx
@@ -2,10 +2,10 @@
title: Database Access Control
---
-This feature requires the **Team** or **Enterprise** plan.
-
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.
+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.
+
```mermaid
flowchart LR
U[User] -->|SQL| P[pgconsole]
@@ -19,17 +19,17 @@ Unlike PostgreSQL's built-in role system (`GRANT`/`REVOKE`), pgconsole's access

-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)
diff --git a/docs/features/mcp-server.mdx b/docs/features/mcp-server.mdx
index 16a990f..c39dc26 100644
--- a/docs/features/mcp-server.mdx
+++ b/docs/features/mcp-server.mdx
@@ -24,7 +24,7 @@ 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:`. Audited as `agent:`.
- **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.
diff --git a/docs/features/white-labeling.mdx b/docs/features/white-labeling.mdx
index 7624334..c3ec62e 100644
--- a/docs/features/white-labeling.mdx
+++ b/docs/features/white-labeling.mdx
@@ -2,8 +2,6 @@
title: White Labeling
---
-This feature requires the **Enterprise** plan.
-
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.
diff --git a/docs/getting-started/faq.mdx b/docs/getting-started/faq.mdx
index 6c4623d..87e8b91 100644
--- a/docs/getting-started/faq.mdx
+++ b/docs/getting-started/faq.mdx
@@ -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
diff --git a/server/auth-routes.ts b/server/auth-routes.ts
index 166c2b6..b364120 100644
--- a/server/auth-routes.ts
+++ b/server/auth-routes.ts
@@ -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 = {
- google: 'SSO_GOOGLE',
- keycloak: 'SSO_KEYCLOAK',
- okta: 'SSO_OKTA',
-}
const router = Router()
@@ -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)
@@ -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 })
diff --git a/server/index.ts b/server/index.ts
index 43fbb17..2c1a731 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -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
@@ -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(),
})
})
@@ -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()
diff --git a/server/lib/audit.ts b/server/lib/audit.ts
index d982155..23ba896 100644
--- a/server/lib/audit.ts
+++ b/server/lib/audit.ts
@@ -1,7 +1,4 @@
// Audit logging - emits JSON lines to stdout
-import { feature } from '../../src/lib/plan'
-import { getPlan } from './config'
-
interface BaseEvent {
type: 'audit'
ts: string
@@ -49,7 +46,6 @@ interface DataExportEvent extends BaseEvent {
type AuditEvent = AuthLoginEvent | AuthLogoutEvent | SQLExecuteEvent | DataExportEvent
function emit(event: AuditEvent): void {
- if (!feature('AUDIT_LOG', getPlan())) return
console.log(JSON.stringify(event))
}
diff --git a/server/lib/config.ts b/server/lib/config.ts
index 556e3b9..ebd9169 100644
--- a/server/lib/config.ts
+++ b/server/lib/config.ts
@@ -1,8 +1,5 @@
import { parse } from 'smol-toml'
import { readFileSync } from 'fs'
-import { checkLicense } from './license'
-import { feature } from '../../src/lib/plan'
-import type { PlanTier } from '../../src/lib/plan'
import type { Vendor } from '../ai/vendors'
export interface LabelConfig {
@@ -103,7 +100,6 @@ export interface IAMRule {
interface Config {
external_url?: string
- license?: string
banner?: BannerConfig
branding?: BrandingConfig
users: UserConfig[]
@@ -114,10 +110,6 @@ interface Config {
ai?: AIConfig
agents: AgentConfig[]
iam: IAMRule[]
- plan: PlanTier
- licenseExpiry?: number
- licenseMaxUsers: number
- licenseEmail?: string
}
const validSslModes = ['disable', 'prefer', 'require', 'verify-full']
@@ -144,7 +136,7 @@ function parsePermissionList(raw: unknown, label: string): Permission[] {
return permissions
}
-const DEFAULT_CONFIG: Config = { users: [], groups: [], labels: [], connections: [], auth: undefined, ai: undefined, agents: [], banner: undefined, branding: undefined, license: undefined, iam: [], plan: 'FREE', licenseExpiry: undefined, licenseMaxUsers: 1, licenseEmail: undefined }
+const DEFAULT_CONFIG: Config = { users: [], groups: [], labels: [], connections: [], auth: undefined, ai: undefined, agents: [], banner: undefined, branding: undefined, iam: [] }
let loadedConfig: Config = { ...DEFAULT_CONFIG }
let demoMode = false
@@ -183,7 +175,6 @@ export async function loadConfigFromString(content: string): Promise {
// Parse [general] section
let external_url: string | undefined = undefined
- let license: string | undefined = undefined
let banner: BannerConfig | undefined = undefined
if (parsed.general) {
const g = parsed.general
@@ -201,13 +192,6 @@ export async function loadConfigFromString(content: string): Promise {
}
}
- if (g.license !== undefined) {
- if (typeof g.license !== 'string') {
- throw new Error('general.license must be a string')
- }
- license = g.license
- }
-
// Parse [general.banner] section
const rawBanner = g.banner as Record | undefined
if (rawBanner) {
@@ -763,28 +747,7 @@ export async function loadConfigFromString(content: string): Promise {
iam.push({ connection, permissions, members })
}
- // Resolve license → plan tier and maxUsers before assigning config
- let plan: PlanTier = 'FREE'
- let licenseExpiry: number | undefined
- let licenseMaxUsers = 1
- let licenseEmail: string | undefined
- if (license) {
- const result = await checkLicense(license)
- plan = result.plan
- licenseExpiry = result.expiry
- licenseMaxUsers = result.maxUsers
- licenseEmail = result.email
- }
-
- loadedConfig = { external_url, license, banner, branding, users, groups, labels, connections, auth, ai, agents, iam, plan, licenseExpiry, licenseMaxUsers, licenseEmail }
-
- // Validate user count against license limit
- if (auth) {
- const limit = licenseMaxUsers
- if (users.length > limit) {
- throw new Error(`Too many [[users]] entries: ${users.length} configured but current license only allows ${limit}. Remove users or upgrade at https://docs.pgconsole.com/configuration/license#purchasing-a-license`)
- }
- }
+ loadedConfig = { external_url, banner, branding, users, groups, labels, connections, auth, ai, agents, iam }
}
export function getLabels(): LabelConfig[] {
@@ -800,7 +763,6 @@ export function getGroupById(id: string): GroupConfig | undefined {
}
export function getGroupsForUser(email: string): GroupConfig[] {
- if (!feature('GROUPS', getPlan())) return []
return loadedConfig.groups.filter((g) => g.members.includes(email))
}
@@ -877,26 +839,6 @@ export function getIAMRules(): IAMRule[] {
return loadedConfig.iam
}
-export function getLicense(): string | undefined {
- return loadedConfig.license
-}
-
-export function getPlan(): PlanTier {
- return loadedConfig.plan
-}
-
-export function getLicenseExpiry(): number | undefined {
- return loadedConfig.licenseExpiry
-}
-
-export function getLicenseMaxUsers(): number {
- return loadedConfig.licenseMaxUsers
-}
-
-export function getLicenseEmail(): string | undefined {
- return loadedConfig.licenseEmail
-}
-
export function loadDemoConfig(port: number): void {
demoMode = true
loadedConfig = {
diff --git a/server/lib/iam.ts b/server/lib/iam.ts
index c951d8b..6745136 100644
--- a/server/lib/iam.ts
+++ b/server/lib/iam.ts
@@ -1,6 +1,5 @@
import { ConnectError, Code } from '@connectrpc/connect'
-import { getIAMRules, getGroupsForUser, isAuthEnabled, getPlan } from './config'
-import { feature } from '../../src/lib/plan'
+import { getIAMRules, getGroupsForUser, isAuthEnabled } from './config'
import type { Permission, AgentConfig, IAMRule } from './config'
export type { Permission }
@@ -63,13 +62,15 @@ export function requireAnyPermission(
}
// Union the permissions from every IAM rule that applies to this connection and whose
-// members satisfy `matches`. Auth-disabled / IAM-not-licensed short-circuit to full access.
+// members satisfy `matches`. IAM is opt-in: with auth disabled or no [[iam]] rules
+// defined, every principal gets full access. Defining the first rule turns enforcement on.
function resolvePermissions(connectionId: string, matches: (rule: IAMRule) => boolean): Set {
- if (!isAuthEnabled() || !feature('IAM', getPlan())) {
+ const rules = getIAMRules()
+ if (!isAuthEnabled() || rules.length === 0) {
return new Set(ALL_PERMISSIONS)
}
const permissions = new Set()
- for (const rule of getIAMRules()) {
+ for (const rule of rules) {
if (rule.connection !== '*' && rule.connection !== connectionId) {
continue
}
@@ -88,7 +89,7 @@ function resolvePermissions(connectionId: string, matches: (rule: IAMRule) => bo
*/
export function getUserPermissions(email: string, connectionId: string): Set {
// Resolve the user's groups lazily — skipped entirely when resolvePermissions
- // short-circuits (auth off / IAM unlicensed) or when no rule has a group: member.
+ // short-circuits (auth off / no IAM rules) or when no rule has a group: member.
let groupIds: Set | undefined
return resolvePermissions(connectionId, rule =>
rule.members.some(member => {
@@ -112,9 +113,8 @@ export function hasPermission(email: string, connectionId: string, permission: P
/**
* Get an agent's effective permissions for a connection. Like `getUserPermissions`, this
- * grants full access when auth is disabled or the plan doesn't include IAM (the shared
- * `resolvePermissions` short-circuit — IAM is a paid feature, applied uniformly to every
- * principal). When IAM is active:
+ * grants full access when auth is disabled or no IAM rules are defined (the shared
+ * `resolvePermissions` short-circuit). Once IAM is active:
* - Pure agent: matched ONLY by explicit `agent:` rules — `*`/`group:`/`user:` rules
* never apply, so no agent silently inherits a broad "everyone" grant.
* - Delegated agent (`onBehalfOf`): the user's grant, narrowed by the connection/permission
diff --git a/server/lib/license.ts b/server/lib/license.ts
deleted file mode 100644
index 31f8470..0000000
--- a/server/lib/license.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { jwtVerify, importSPKI } from 'jose'
-import type { PlanTier } from '../../src/lib/plan'
-
-export interface LicenseResult {
- plan: PlanTier
- expiry?: number
- maxUsers: number
- email?: string
-}
-
-const LICENSE_ISSUER = 'pgconsole/license'
-
-let KEYGEN_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNGFvPtpvyhT7eYc1x5Y
-ir/nW5CAH3kLYL3F70xN5bMYxsx9h9H/4xBIAk3ddm/maOqcua2E+PI2Z1w8lEtR
-GhAXSJtykKGuPXIDudnMVXbYqYhvvVYTH4NXRtTuS5NdTD0ZURr6X8X01dIsJVve
-QUp/TXODV0GHTRvkRdisak3NUnub9Mv20XirYWPed1OnDWLuE57T1FaD6ZYhC0is
-loNCg5i7KfmNxigW/iABe7Pbvafuq5O5UBkN9l7x+kcnc68oY/ceBUnyHa8Hj3p6
-B5IyYLTs5y7dD1IS22hJiteOQEmmdOQpYCXyOhVRXcuVHzHYkQNNvlW9vDVTE+m7
-rQIDAQAB
------END PUBLIC KEY-----`
-
-export function setPublicKeyForTesting(pem: string): void {
- KEYGEN_PUBLIC_KEY = pem
-}
-
-export async function checkLicense(license: string): Promise {
- try {
- const publicKey = await importSPKI(KEYGEN_PUBLIC_KEY, 'RS256')
- const { payload } = await jwtVerify(license, publicKey, {
- issuer: LICENSE_ISSUER,
- requiredClaims: ['exp'],
- })
- const plan = payload.plan as string
- const maxUsers = typeof payload.userSeat === 'number' ? payload.userSeat : 1
- const email = typeof payload.email === 'string' ? payload.email : undefined
- if (plan === 'team' || plan === 'enterprise') {
- return { plan: plan.toUpperCase() as PlanTier, expiry: payload.exp, maxUsers, email }
- }
- console.warn('License JWT has unrecognized plan claim:', plan)
- return { plan: 'FREE', maxUsers: 1 }
- } catch (err) {
- console.warn('License verification failed:', err instanceof Error ? err.message : err)
- return { plan: 'FREE', maxUsers: 1 }
- }
-}
diff --git a/src/App.tsx b/src/App.tsx
index b2a8195..77bb180 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -12,8 +12,6 @@ import { useEditorNavigation } from './hooks/useEditorNavigation';
import { Button } from './components/ui/button';
import { Banner } from './components/Banner';
import { useSetting } from './hooks/useSetting';
-import { SubscriptionModalProvider } from './components/SubscriptionModal';
-import { LicenseExpiryBanner } from './components/LicenseExpiryBanner';
import { DemoBanner } from './components/DemoBanner';
function AppLayout() {
@@ -103,13 +101,11 @@ function AppLayout() {
return (
-
-
}
- features={['1 user', 'Unlimited connections', 'SQL editor and intellisense', 'Inline data editing', 'Schema inspection', 'AI assistant']}
- cta={
-
- Get started
-
- }
- />
- For teams who need collaboration}
- features={[
- 'Everything in Personal',
- 'Unlimited users',
- 'User groups',
- 'Database access control',
- 'Google SSO',
- 'Custom banner',
- ]}
- cta={
-
- Get started
-
- }
- />
- For organizations that need security and governance}
- features={[
- 'Everything in Team',
- 'Enterprise SSO (Okta, Keycloak, etc.)',
- 'Audit logging',
- 'OEM / White-labeling',
- 'SOC 2 report',
- 'Dedicated support channel',
- ]}
- cta={
-
- Contact us
-
- }
- />
- >
- )
-}
-
-export default function PricingPage() {
- return (
- <>
-
-
-
- {/* Pricing */}
-
- Start in minutes and scale as you grow.
-
- }
- options={['Monthly', 'Yearly']}
- plans={{ Monthly: plans('Monthly'), Yearly: plans('Yearly') }}
- />
-
- {/* FAQs */}
-
-
-
-
-
-
-
- {/* Call To Action */}
-
-
-
-
- >
- )
-}
diff --git a/website/src/app/sitemap.ts b/website/src/app/sitemap.ts
index af7f19c..824d357 100644
--- a/website/src/app/sitemap.ts
+++ b/website/src/app/sitemap.ts
@@ -8,7 +8,6 @@ export default function sitemap(): MetadataRoute.Sitemap {
return [
{ url: BASE_URL, lastModified: new Date() },
- { url: `${BASE_URL}/pricing`, lastModified: new Date() },
{ url: `${BASE_URL}/blog`, lastModified: posts[0]?.date ?? new Date() },
...posts.map((post) => ({
url: `${BASE_URL}/blog/${post.slug}`,
diff --git a/website/src/components/sections/plan-comparison-table.tsx b/website/src/components/sections/plan-comparison-table.tsx
deleted file mode 100644
index ed30668..0000000
--- a/website/src/components/sections/plan-comparison-table.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import { ElTabGroup, ElTabList, ElTabPanels } from '@tailwindplus/elements/react'
-import { clsx } from 'clsx/lite'
-import { type ComponentProps, type ReactNode } from 'react'
-import { Container } from '../elements/container'
-import { CheckmarkIcon } from '../icons/checkmark-icon'
-import { MinusIcon } from '../icons/minus-icon'
-
-function FeatureGroup({
- group,
- plans,
-}: {
- group: {
- title: ReactNode
- features: { name: ReactNode; value: ReactNode | Record }[]
- }
- plans: Plan[]
-}) {
- return (
-
-
-
- {group.title}
-
-
- {group.features.map((feature) => (
-
-
- {feature.name}
-
- {plans.map((plan) => {
- const value = ((value: any): value is Record =>
- typeof value === 'object' && value !== null && plan in value)(feature.value)
- ? feature.value[plan]
- : feature.value
-
- return (
-
+ } />
+ } />
+
+
);
From 86d163fd0ea27fab0fd6279da208c956b4a0bfef Mon Sep 17 00:00:00 2001
From: tianzhou
Date: Thu, 25 Jun 2026 02:01:27 -0700
Subject: [PATCH 3/4] chore: remove stripe worker deploy workflow and scrub
example config
Follow-up to the pricing removal, addressing PR review:
- Delete .github/workflows/deploy-stripe-worker.yml, which referenced
the now-deleted worker/stripe-webhook and would fail on merge.
- Remove the commented license key and "Enterprise only" branding notes
from pgconsole.example.toml to match the all-features-free model.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.github/workflows/deploy-stripe-worker.yml | 40 ----------------------
pgconsole.example.toml | 5 ++-
2 files changed, 2 insertions(+), 43 deletions(-)
delete mode 100644 .github/workflows/deploy-stripe-worker.yml
diff --git a/.github/workflows/deploy-stripe-worker.yml b/.github/workflows/deploy-stripe-worker.yml
deleted file mode 100644
index 67e7514..0000000
--- a/.github/workflows/deploy-stripe-worker.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-name: Deploy Stripe Webhook Worker
-
-on:
- push:
- branches:
- - main
- paths:
- - "worker/stripe-webhook/**"
- - ".github/workflows/deploy-stripe-worker.yml"
- workflow_dispatch:
-
-defaults:
- run:
- working-directory: worker/stripe-webhook
-
-jobs:
- deploy:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
- with:
- node-version: "20"
- - run: npm ci
-
- - name: Deploy
- run: npx wrangler deploy
- env:
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
-
- - name: Set secrets
- run: |
- echo "${{ secrets.STRIPE_WEBHOOK_SECRET_LIVE }}" | npx wrangler secret put STRIPE_WEBHOOK_SECRET_LIVE
- echo "${{ secrets.STRIPE_WEBHOOK_SECRET_TEST }}" | npx wrangler secret put STRIPE_WEBHOOK_SECRET_TEST
- echo "${{ secrets.RESEND_API_KEY }}" | npx wrangler secret put RESEND_API_KEY
- echo "${{ secrets.KEYGEN_API_KEY }}" | npx wrangler secret put KEYGEN_API_KEY
- env:
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
diff --git a/pgconsole.example.toml b/pgconsole.example.toml
index 0fc70a4..aa8598b 100644
--- a/pgconsole.example.toml
+++ b/pgconsole.example.toml
@@ -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]
@@ -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"
From 95b5ea5883c3f1ad229cd3e9a7c554b6f13c1a67 Mon Sep 17 00:00:00 2001
From: Tianzhou
Date: Thu, 25 Jun 2026 02:12:56 -0700
Subject: [PATCH 4/4] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
docs/features/mcp-server.mdx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/docs/features/mcp-server.mdx b/docs/features/mcp-server.mdx
index c39dc26..6ac549b 100644
--- a/docs/features/mcp-server.mdx
+++ b/docs/features/mcp-server.mdx
@@ -26,7 +26,9 @@ Requests without a valid token are rejected with `401`.
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:`. Audited as `agent:`.
+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:`. Audited as `agent:`.
- **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.
```toml pgconsole.toml