> ## Documentation Index
> Fetch the complete documentation index at: https://planetscale.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication attempts

> Export a report of every attempt to authenticate against your Postgres databases, including the real client IP, the credential used, and whether access was allowed or denied.

export const PlatformAvailability = ({current, vitess, postgres}) => {
  const docsHref = path => {
    if (!path) return path;
    const normalized = path.startsWith('/') ? path : `/${path}`;
    return normalized;
  };
  const labels = {
    vitess: 'Vitess',
    postgres: 'Postgres'
  };
  if (current === 'both') {
    return <div className="not-prose mb-5 flex flex-wrap items-center gap-2" role="group" aria-label="Platform availability">
        <span data-engine="both" data-state="current" aria-current="true" className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border px-2.5 py-1 text-[13px] font-semibold leading-tight no-underline data-[engine=vitess]:data-[state=current]:border-[#ffc59b] data-[engine=vitess]:data-[state=current]:bg-[#ffe8d8] data-[engine=vitess]:data-[state=current]:text-[#672002] dark:data-[engine=vitess]:data-[state=current]:border-[#962d00] dark:data-[engine=vitess]:data-[state=current]:bg-[#3c1403] dark:data-[engine=vitess]:data-[state=current]:text-[#ffe8d8] data-[engine=vitess]:data-[state=link]:border-[#ffc59b] data-[engine=vitess]:data-[state=link]:bg-transparent data-[engine=vitess]:data-[state=link]:text-[#b83a05] dark:data-[engine=vitess]:data-[state=link]:border-[#962d00] dark:data-[engine=vitess]:data-[state=link]:bg-transparent dark:data-[engine=vitess]:data-[state=link]:text-[#ffc59b] data-[engine=postgres]:data-[state=current]:border-[#a9dffe] data-[engine=postgres]:data-[state=current]:bg-[#ddf2ff] data-[engine=postgres]:data-[state=current]:text-[#0e3682] dark:data-[engine=postgres]:data-[state=current]:border-[#144eb6] dark:data-[engine=postgres]:data-[state=current]:bg-[#08204e] dark:data-[engine=postgres]:data-[state=current]:text-[#ddf2ff] data-[engine=postgres]:data-[state=link]:border-[#a9dffe] data-[engine=postgres]:data-[state=link]:bg-transparent data-[engine=postgres]:data-[state=link]:text-[#0b6ec5] dark:data-[engine=postgres]:data-[state=link]:border-[#144eb6] dark:data-[engine=postgres]:data-[state=link]:bg-transparent dark:data-[engine=postgres]:data-[state=link]:text-[#73c7f9] data-[engine=both]:data-[state=current]:border-[#d4d4d4] data-[engine=both]:data-[state=current]:bg-[#f0f0f0] data-[engine=both]:data-[state=current]:text-[#3d3d3d] dark:data-[engine=both]:data-[state=current]:border-[#525252] dark:data-[engine=both]:data-[state=current]:bg-[#2a2a2a] dark:data-[engine=both]:data-[state=current]:text-[#e5e5e5]">
          Vitess and Postgres
        </span>
      </div>;
  }
  const hasVitess = current === 'vitess' || Boolean(vitess);
  const hasPostgres = current === 'postgres' || Boolean(postgres);
  const only = !(hasVitess && hasPostgres);
  const engines = [];
  if (current === 'vitess' || current === 'postgres') engines.push(current);
  if (hasVitess && current !== 'vitess') engines.push('vitess');
  if (hasPostgres && current !== 'postgres') engines.push('postgres');
  return <div className="not-prose mb-5 flex flex-wrap items-center gap-2" role="group" aria-label="Platform availability">
      {engines.map(engine => {
    const isCurrent = current === engine;
    const href = docsHref(engine === 'vitess' ? vitess : postgres);
    const label = only ? `${labels[engine]} only` : labels[engine];
    const state = isCurrent || !href ? 'current' : 'link';
    if (isCurrent || !href) {
      return <span key={engine} data-engine={engine} data-state={state} aria-current={isCurrent ? 'true' : undefined} className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border px-2.5 py-1 text-[13px] font-semibold leading-tight no-underline data-[engine=vitess]:data-[state=current]:border-[#ffc59b] data-[engine=vitess]:data-[state=current]:bg-[#ffe8d8] data-[engine=vitess]:data-[state=current]:text-[#672002] dark:data-[engine=vitess]:data-[state=current]:border-[#962d00] dark:data-[engine=vitess]:data-[state=current]:bg-[#3c1403] dark:data-[engine=vitess]:data-[state=current]:text-[#ffe8d8] data-[engine=vitess]:data-[state=link]:border-[#ffc59b] data-[engine=vitess]:data-[state=link]:bg-transparent data-[engine=vitess]:data-[state=link]:text-[#b83a05] dark:data-[engine=vitess]:data-[state=link]:border-[#962d00] dark:data-[engine=vitess]:data-[state=link]:bg-transparent dark:data-[engine=vitess]:data-[state=link]:text-[#ffc59b] data-[engine=postgres]:data-[state=current]:border-[#a9dffe] data-[engine=postgres]:data-[state=current]:bg-[#ddf2ff] data-[engine=postgres]:data-[state=current]:text-[#0e3682] dark:data-[engine=postgres]:data-[state=current]:border-[#144eb6] dark:data-[engine=postgres]:data-[state=current]:bg-[#08204e] dark:data-[engine=postgres]:data-[state=current]:text-[#ddf2ff] data-[engine=postgres]:data-[state=link]:border-[#a9dffe] data-[engine=postgres]:data-[state=link]:bg-transparent data-[engine=postgres]:data-[state=link]:text-[#0b6ec5] dark:data-[engine=postgres]:data-[state=link]:border-[#144eb6] dark:data-[engine=postgres]:data-[state=link]:bg-transparent dark:data-[engine=postgres]:data-[state=link]:text-[#73c7f9] data-[engine=both]:data-[state=current]:border-[#d4d4d4] data-[engine=both]:data-[state=current]:bg-[#f0f0f0] data-[engine=both]:data-[state=current]:text-[#3d3d3d] dark:data-[engine=both]:data-[state=current]:border-[#525252] dark:data-[engine=both]:data-[state=current]:bg-[#2a2a2a] dark:data-[engine=both]:data-[state=current]:text-[#e5e5e5]">
              {label}
            </span>;
    }
    return <a key={engine} href={href} data-engine={engine} data-state={state} title={`View ${labels[engine]} documentation`} className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border px-2.5 py-1 text-[13px] font-semibold leading-tight no-underline data-[engine=vitess]:data-[state=current]:border-[#ffc59b] data-[engine=vitess]:data-[state=current]:bg-[#ffe8d8] data-[engine=vitess]:data-[state=current]:text-[#672002] dark:data-[engine=vitess]:data-[state=current]:border-[#962d00] dark:data-[engine=vitess]:data-[state=current]:bg-[#3c1403] dark:data-[engine=vitess]:data-[state=current]:text-[#ffe8d8] data-[engine=vitess]:data-[state=link]:border-[#ffc59b] data-[engine=vitess]:data-[state=link]:bg-transparent data-[engine=vitess]:data-[state=link]:text-[#b83a05] dark:data-[engine=vitess]:data-[state=link]:border-[#962d00] dark:data-[engine=vitess]:data-[state=link]:bg-transparent dark:data-[engine=vitess]:data-[state=link]:text-[#ffc59b] data-[engine=postgres]:data-[state=current]:border-[#a9dffe] data-[engine=postgres]:data-[state=current]:bg-[#ddf2ff] data-[engine=postgres]:data-[state=current]:text-[#0e3682] dark:data-[engine=postgres]:data-[state=current]:border-[#144eb6] dark:data-[engine=postgres]:data-[state=current]:bg-[#08204e] dark:data-[engine=postgres]:data-[state=current]:text-[#ddf2ff] data-[engine=postgres]:data-[state=link]:border-[#a9dffe] data-[engine=postgres]:data-[state=link]:bg-transparent data-[engine=postgres]:data-[state=link]:text-[#0b6ec5] dark:data-[engine=postgres]:data-[state=link]:border-[#144eb6] dark:data-[engine=postgres]:data-[state=link]:bg-transparent dark:data-[engine=postgres]:data-[state=link]:text-[#73c7f9] data-[engine=both]:data-[state=current]:border-[#d4d4d4] data-[engine=both]:data-[state=current]:bg-[#f0f0f0] data-[engine=both]:data-[state=current]:text-[#3d3d3d] dark:data-[engine=both]:data-[state=current]:border-[#525252] dark:data-[engine=both]:data-[state=current]:bg-[#2a2a2a] dark:data-[engine=both]:data-[state=current]:text-[#e5e5e5]">
            {label}
            <svg aria-hidden="true" width="12" height="12" viewBox="0 0 12 12" fill="none" className="shrink-0">
              <path d="M2.5 6h7M6.5 3l3 3-3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </a>;
  })}
    </div>;
};

<PlatformAvailability current="postgres" />

Authentication attempt reports provide a record of attempts to authenticate against your Postgres databases. You can use them to review connection activity, troubleshoot authentication failures, investigate unexpected access, or export authentication data for further analysis.

Each record includes information such as the client IP address, credential used, targeted database and branch, and whether the attempt was allowed or denied.

The `source_ip` in the report is the address of the client that connected to PlanetScale. This is especially useful for connections routed through a pooler, where Postgres server logs may show a PlanetScale infrastructure address instead of the original client address.

<Note>
  Generating a report is limited to [Organization Administrators](/docs/security/access-control#organization-administrator). Service tokens cannot generate reports.
</Note>

## Generate a report

Use the [`pscale` CLI](/docs/cli/audit-log) to generate and download a report. Specify a time window and, optionally, filters:

```bash theme={null}
pscale audit-log auth-attempts download --since 24h --output auth-attempts.zip
```

```bash theme={null}
Generating authentication-attempt export for acme...
Successfully downloaded auth attempts (csv) to auth-attempts.zip
```

The download link expires 24 hours after the report is generated. If you need to download the report again after that, generate a new report.

## What's in the archive

Every archive contains exactly two files:

| File                                          | Contents                                                            |
| :-------------------------------------------- | :------------------------------------------------------------------ |
| `auth-attempts.csv` (or `.jsonl`, `.parquet`) | One row per authentication attempt                                  |
| `manifest.json`                               | The time window, filters, format, and other metadata for the report |

### The data

| Column                                 | Description                                                 |
| :------------------------------------- | :---------------------------------------------------------- |
| `occurred_at`                          | When the attempt happened, UTC, to the millisecond          |
| `source_ip`                            | The IP address the client connected from                    |
| `source_port`                          | The client's source port                                    |
| `organization_id`, `organization_name` | The organization                                            |
| `database_id`, `database_name`         | The database the attempt targeted                           |
| `branch_id`, `branch_name`             | The branch the attempt targeted                             |
| `username`                             | The credential used                                         |
| `startup_database`                     | The database named in the connection's startup message      |
| `outcome`                              | `allow` or `deny`                                           |
| `failure_reason`                       | Why it was denied. Empty when the attempt succeeded         |
| `sqlstate`                             | The Postgres SQLSTATE returned on failure                   |
| `backend_route`                        | `postgres` or `pgbouncer`, depending on the connection path |
| `auth_protocol`                        | The wire protocol used                                      |

Denials carry one of these reasons:

| `failure_reason`       | Meaning                                              |
| :--------------------- | :--------------------------------------------------- |
| `bad_password`         | The credential exists but the password did not match |
| `unknown_user`         | No such credential                                   |
| `authorization_failed` | Authenticated, but not permitted to proceed          |
| `ip_not_allowed`       | Blocked by an IP allow list                          |
| `other`                | Anything else                                        |

### The manifest

`manifest.json` records how the report was generated:

```json theme={null}
{
  "schema_version": 1,
  "format": "csv",
  "generated_at": "2026-08-10T14:46:20Z",
  "organization": { "id": "...", "name": "acme" },
  "window": {
    "start_at": "2026-08-09T00:00:00Z",
    "end_at": "2026-08-10T00:00:00Z",
    "semantics": "[start_at,end_at)"
  },
  "filters": { "source_ips": ["203.0.113.10/32"] },
  "resolved_branch_public_ids": ["..."]
}
```

Keep the manifest with the exported data when storing or sharing a report. It records the time window and filters used to produce the export.

The report window uses `[start_at,end_at)` semantics: `start_at` is inclusive and `end_at` is exclusive.

## Example queries

### Filter by source IP

Pass a single address or a whole CIDR range:

```bash theme={null}
pscale audit-log auth-attempts download --since 7d \
  --source-ip 203.0.113.10 --output suspect-ip.zip
```

```bash theme={null}
pscale audit-log auth-attempts download --since 7d \
  --source-ip 203.0.113.0/24 --source-ip 198.51.100.0/24 --output suspect-range.zip
```

### Filter by credential

Use `--username` to find attempts made with a particular credential. For example, to return only denied attempts:

```bash theme={null}
pscale audit-log auth-attempts download --since 7d \
  --username app_readonly --outcome deny --output credential.zip
```

### Export a time window for analysis

Omit filters to export all authentication attempts in a time window.

For larger datasets, Parquet can be useful for analysis with tools such as DuckDB:

```bash theme={null}
pscale audit-log auth-attempts download \
  --start-at 2026-07-29T00:00:00Z --end-at 2026-07-30T00:00:00Z \
  --export-format parquet --output window.zip

unzip -q window.zip -d window
duckdb -c "
  SELECT username, source_ip, count(*) AS attempts
  FROM 'window/auth-attempts.parquet'
  WHERE outcome = 'deny'
  GROUP BY username, source_ip
  ORDER BY attempts DESC
  LIMIT 20;"
```

Because the archive includes its manifest, you can also share or retain the complete archive without separately recording the parameters used to generate it.

## Combining filters

Filters combine with AND across different flags, and OR within a repeated flag. This request means "denied attempts, from either of these two ranges, using either of these two credentials":

```bash theme={null}
pscale audit-log auth-attempts download --since 24h \
  --outcome deny \
  --source-ip 203.0.113.0/24 --source-ip 198.51.100.0/24 \
  --username app_readonly --username reporting
```

## Choosing a format

| Format    | Good for                                                                                 |
| :-------- | :--------------------------------------------------------------------------------------- |
| `csv`     | Spreadsheets, quick inspection, most SIEM ingest paths                                   |
| `jsonl`   | Log pipelines and tools that process one object per line                                 |
| `parquet` | Analysis over larger datasets and querying with tools such as DuckDB or a data warehouse |

CSV is the default. JSONL is selected by `--format json`, and Parquet must be requested with `--export-format parquet`.

## Need help?

Get help from [the PlanetScale Support team](https://planetscale.com/contact?initial=support), or join our [Discord community](https://pscale.link/community) to see how others are using PlanetScale.
