---
title: "PostgreSQL 15+ Permissions: Why Your Helm Deployment Cannot Create Tables"
description: "PostgreSQL 15 changed default permissions on the public schema. Learn why GRANT ALL is not enough, how to fix Helm init scripts, and how to debug permission denied errors."
url: https://agent-zone.ai/knowledge/kubernetes/postgres15-permissions/
section: knowledge
date: 2026-02-21
categories: ["kubernetes"]
tags: ["postgresql","permissions","helm","bitnami","schema"]
skills: ["postgres-permissions","helm-init-scripts"]
tools: ["postgresql","helm","kubectl","psql"]
levels: ["intermediate"]
word_count: 731
formats:
  json: https://agent-zone.ai/knowledge/kubernetes/postgres15-permissions/index.json
  html: https://agent-zone.ai/knowledge/kubernetes/postgres15-permissions/?format=html
  api: https://api.agent-zone.ai/api/v1/knowledge/search?q=PostgreSQL+15%2B+Permissions%3A+Why+Your+Helm+Deployment+Cannot+Create+Tables
---


# PostgreSQL 15+ Permissions: Why Your Helm Deployment Cannot Create Tables

Starting with PostgreSQL 15, only the database owner and superusers can create objects in the `public` schema by default. This breaks a common Helm pattern where you create a user, grant privileges, and expect it to create tables. The application connects fine but fails on its first `CREATE TABLE`.

## The Symptom

Your application pod logs show something like:

```
Error: permission denied for schema public
```

Or from an ORM like Mattermost's:

```
Failed to create table: pq: permission denied for schema public
```

The confusing part is that the connection itself works. The user can `SELECT` from existing tables. It only fails on `CREATE TABLE`, `CREATE INDEX`, or other DDL operations.

## Why GRANT ALL Is Not Enough

Before PostgreSQL 15, this was sufficient:

```sql
CREATE USER mmuser WITH PASSWORD 'secret';
CREATE DATABASE mattermost OWNER postgres;
GRANT ALL PRIVILEGES ON DATABASE mattermost TO mmuser;
```

In PostgreSQL 15+, `GRANT ALL PRIVILEGES ON DATABASE` grants `CONNECT`, `CREATE`, and `TEMPORARY` on the database itself, but the `public` schema inside that database has a separate permission layer. By default, only the schema owner (usually `postgres`) can create objects in it.

## The Fix

You need to change the **owner** of both the database and the `public` schema to the application user:

```sql
-- Create the user and database
CREATE USER mmuser WITH PASSWORD 'secret';
CREATE DATABASE mattermost;

-- Transfer ownership (this is the critical part)
ALTER DATABASE mattermost OWNER TO mmuser;

-- Connect to the new database and fix schema ownership
-- (must be done while connected to the mattermost database)
ALTER SCHEMA public OWNER TO mmuser;
```

The `ALTER SCHEMA public OWNER TO mmuser` is the line that most guides miss. Without it, `mmuser` can connect to the database but cannot create tables in the default schema.

## Helm Init Script: The Right Way

With the Bitnami PostgreSQL chart, you configure init scripts via `initdb.scripts`. There are two important constraints:

1. **These scripts run only on first boot.** A semaphore file prevents re-execution. If you change the script, you must delete the PVC.
2. **The `\c` psql metacommand is unreliable in heredocs passed through `kubectl exec`.** Use separate `psql` commands instead.

Here is a working `values.yaml` configuration:

```yaml
auth:
  postgresPassword: "adminpass"
  database: mattermost
  username: mmuser
  password: "secret"

initdbScripts:
  init-permissions.sh: |
    #!/bin/bash
    set -e

    # The Bitnami chart already creates the database and user from the auth.* values.
    # We just need to fix ownership for PostgreSQL 15+ compatibility.

    PGPASSWORD="$POSTGRES_PASSWORD" psql -U postgres -d mattermost -c \
      "ALTER SCHEMA public OWNER TO mmuser;"

    PGPASSWORD="$POSTGRES_PASSWORD" psql -U postgres -d mattermost -c \
      "ALTER DATABASE mattermost OWNER TO mmuser;"

    echo "Permissions configured for PostgreSQL 15+"
```

Note the use of a shell script (`.sh` extension) rather than a raw SQL file. This gives you access to environment variables and lets you run multiple `psql` commands with explicit `-d` database targeting.

### Why Not Use a .sql File?

A `.sql` file tempts you to use `\c mattermost` to switch databases. But `\c` is a psql client metacommand, not SQL. When the Bitnami chart pipes SQL files into psql, `\c` sometimes fails silently, especially through shell heredocs or `kubectl exec` chains. Using separate `psql -d <database>` calls is reliable every time.

## Debugging Permission Issues

**1. Verify the connection works** -- if this fails, the problem is authentication, not permissions:

```bash
kubectl exec -it dt-postgresql-0 -n dream-team -- \
  psql -U mmuser -d mattermost -c "SELECT 1;"
```

**2. Check schema ownership** -- if `Owner` shows `postgres` instead of your app user, that is the problem:

```bash
kubectl exec -it dt-postgresql-0 -n dream-team -- \
  psql -U postgres -d mattermost -c "\dn+"
#  Name  |  Owner   | Access privileges |      Description
# public | postgres | postgres=UC/...   | standard public schema
```

**3. Fix it live:**

```bash
kubectl exec -it dt-postgresql-0 -n dream-team -- \
  psql -U postgres -d mattermost -c "ALTER SCHEMA public OWNER TO mmuser;"
kubectl exec -it dt-postgresql-0 -n dream-team -- \
  psql -U postgres -d mattermost -c "ALTER DATABASE mattermost OWNER TO mmuser;"
```

**4. Force init script re-run (destroys data -- dev only):**

```bash
kubectl delete pvc data-dt-postgresql-0 -n dream-team
kubectl delete pod dt-postgresql-0 -n dream-team
```

## Summary

| PostgreSQL Version | `GRANT ALL ON DATABASE` | Needs `ALTER SCHEMA public OWNER TO` |
|-|-|-|
| 14 and earlier | Sufficient for DDL | No |
| 15+ | Only grants CONNECT/CREATE/TEMP | **Yes** |

The one-line fix is `ALTER SCHEMA public OWNER TO <your_user>`. Put it in a `.sh` init script with explicit `psql -d <database>` calls, and your Helm-deployed PostgreSQL 15+ will work correctly on first boot.

