# Neon Serverless Postgres Standard

> **Scope:** neon
> **Layer:** 0 (always load)
> **Keywords:** neon, serverless postgres, branching, scale-to-zero, connection string, neonctl
> **Load When:** neon stack detected

**Verified against:** .NET 10 + Npgsql/EF Core 10 + neonctl. Last-verified: 2026-05-20.

---

Stack: .NET + Neon Serverless Postgres

## Core Rules

- ALWAYS use pooled connection strings (`-pooler` endpoint) for application traffic
- ALWAYS use direct connection strings for migrations and schema changes
- NEVER hardcode connection strings -- use environment variables
- ALWAYS enable SSL (`SSL Mode=Require`) for all connections
- ALWAYS configure retry policies for cold-start resilience
- NEVER leave branches without expiration -- set TTL for dev/CI branches

## Neon CLI Setup

```bash
# Install neonctl globally
npm install -g neonctl

# Authenticate (opens browser)
neonctl auth

# Verify connection
neonctl projects list
```

## Connection Strings

### Pooled vs Direct

| Type | Hostname Pattern | Use Case |
|------|-----------------|----------|
| Pooled | `ep-xxxx-pooler.region.aws.neon.tech` | Application queries, high concurrency |
| Direct | `ep-xxxx.region.aws.neon.tech` | Migrations, EF Core `dotnet ef`, advisory locks |

### Environment Variables

| Variable | Purpose |
|----------|---------|
| `DATABASE_URL` | Pooled connection for application runtime |
| `DATABASE_URL_UNPOOLED` | Direct connection for migrations and CLI tools |

```ini
# Pooled (application)
DATABASE_URL="Host=ep-cool-darkness-123456-pooler.us-east-2.aws.neon.tech;Database=mydb;Username=myuser;Password=secret;SSL Mode=Require"

# Direct (migrations)
DATABASE_URL_UNPOOLED="Host=ep-cool-darkness-123456.us-east-2.aws.neon.tech;Database=mydb;Username=myuser;Password=secret;SSL Mode=Require"
```

## EF Core Configuration

### appsettings.json

```json
{
  "ConnectionStrings": {
    "DefaultConnection": "Host=ep-cool-darkness-123456-pooler.us-east-2.aws.neon.tech;Database=mydb;Username=myuser;Password=secret;SSL Mode=Require;Trust Server Certificate=true",
    "MigrationConnection": "Host=ep-cool-darkness-123456.us-east-2.aws.neon.tech;Database=mydb;Username=myuser;Password=secret;SSL Mode=Require;Trust Server Certificate=true"
  }
}
```

### DbContext Registration

```csharp
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(
        builder.Configuration.GetConnectionString("DefaultConnection"),
        npgsqlOptions =>
        {
            npgsqlOptions.EnableRetryOnFailure(
                maxRetryCount: 5,
                maxRetryDelay: TimeSpan.FromSeconds(10),
                errorCodesToAdd: null);
            npgsqlOptions.CommandTimeout(30);
        }));
```

### Design-Time Factory (for migrations using direct connection)

```csharp
public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
    public AppDbContext CreateDbContext(string[] args)
    {
        var configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .AddEnvironmentVariables()
            .Build();

        var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
        optionsBuilder.UseNpgsql(
            configuration.GetConnectionString("MigrationConnection"));

        return new AppDbContext(optionsBuilder.Options);
    }
}
```

## Branching Workflow

Neon branches are copy-on-write clones of your database -- instant, zero-cost until data diverges.

```bash
# List branches
neonctl branches list --project-id <project-id>

# Create feature branch from main
neonctl branches create --project-id <project-id> \
  --name feature/my-feature \
  --parent main

# Create branch with auto-expiration (CI/CD)
neonctl branches create --project-id <project-id> \
  --name ci-test-run-42 \
  --parent main \
  --expires-at "2027-01-01T00:00:00Z"

# Reset branch to parent state
neonctl branches reset feature/my-feature --parent --project-id <project-id>

# Delete branch
neonctl branches delete feature/my-feature --project-id <project-id>
```

### Branch Strategy

| Branch | Purpose | Expiration |
|--------|---------|------------|
| `main` | Production database | Never |
| `dev` | Development shared | Never |
| `feature/*` | Feature development | 7 days |
| `ci-*` | CI test runs | 1 day |

## Scale-to-Zero

Neon automatically suspends computes after inactivity (default: 5 minutes). This saves cost but introduces cold-start latency (~300-500ms on first request).

### Mitigate Cold Starts

```csharp
// 1. Configure retry policy to handle connection drops on wake-up
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(connectionString, npgsqlOptions =>
    {
        npgsqlOptions.EnableRetryOnFailure(
            maxRetryCount: 5,
            maxRetryDelay: TimeSpan.FromSeconds(10),
            errorCodesToAdd: null);
    }));

// 2. Health check to warm the connection pool
builder.Services.AddHealthChecks()
    .AddNpgSql(connectionString, name: "neon-db");
```

### Suspend Timeout Configuration

```bash
# Set suspend timeout to 10 minutes (600 seconds)
neonctl branches create --project-id <project-id> \
  --name main \
  --suspend-timeout 600

# Compute size range (autoscaling)
neonctl branches create --project-id <project-id> \
  --name main \
  --cu "0.25-4"
```

## Common Mistakes

| Wrong | Right | Why |
|-------|-------|-----|
| Direct connection for app queries | Pooled (`-pooler`) connection | Direct connections exhaust compute limits |
| No retry policy | `EnableRetryOnFailure()` | Cold starts drop first connection |
| Hardcoded connection strings | Environment variables | Branches have unique endpoints |
| Permanent CI branches | `--expires-at` TTL | Orphaned branches waste resources |
| Running migrations on pooled connection | Direct connection for `dotnet ef` | PgBouncer doesn't support advisory locks |
| No SSL | `SSL Mode=Require` | Neon requires encrypted connections |
