Repository: benbjohnson/litestream

Issue:
Documentation: Update Example Configurations for AWS SDK v2
## Documentation: Update Example Configurations for AWS SDK v2

### Overview
With the AWS SDK v2 upgrade and new configuration options, we need to update all example configuration files to showcase the new capabilities and best practices.

### Problem
Current example configurations don't demonstrate:
- New multipart upload configuration options
- S3-compatible service configurations
- Performance tuning options
- Various authentication methods
- Best practices for different use cases

### Files to Update

1. `etc/litestream.yml` - Main example configuration
2. Documentation examples on litestream.io
3. README.md examples
4. Docker example configurations

### Proposed Updates

#### 1. Update etc/litestream.yml

```yaml
# Litestream configuration file
#
# This file demonstrates various configuration options available
# after the AWS SDK v2 upgrade.

# Single database replication to S3
dbs:
  - path: /var/lib/myapp/db.sqlite
    replicas:
      - type: s3
        bucket: my-app-backups
        path: db
        region: us-east-1
        
        # AWS SDK v2 Performance Options (optional)
        # part-size: 10MB        # Size of multipart upload parts (min: 5MB)
        # concurrency: 5         # Number of parallel uploads
        # http-timeout: 6h      # Total operation timeout
        
        # Retention (optional)
        retention: 24h
        retention-check-interval: 1h
        
        # Sync interval
        sync-interval: 10s

# Multiple database configuration with different strategies
# dbs:
#   # High-frequency updates, small database
#   - path: /data/cache.db
#     replicas:
#       - type: s3
#         bucket: backups
#         path: cache
#         region: us-west-2
#         sync-interval: 1s      # Frequent syncs
#         # Small database, use defaults for multipart settings
#
#   # Large database, infrequent updates  
#   - path: /data/analytics.db
#     replicas:
#       - type: s3
#         bucket: backups
#         path: analytics
#         region: us-west-2
#         sync-interval: 1h      # Less frequent syncs
#         part-size: 50MB        # Larger parts for efficiency
#         concurrency: 20        # High parallelism
#         http-timeout: 24h     # Long timeout for large uploads

# Monitoring (optional)
# prometheus:
#   url: http://localhost:9090
#   job: litestream
```

#### 2. S3-Compatible Services Examples

Create `etc/examples/s3-compatible.yml`:

```yaml
# Examples for S3-compatible storage services

# MinIO (self-hosted)
dbs:
  - path: /data/app.db
    replicas:
      - type: s3
        endpoint: https://minio.mycompany.com
        bucket: litestream-backups
        path: app-db
        access-key-id: ${MINIO_ACCESS_KEY}
        secret-access-key: ${MINIO_SECRET_KEY}
        force-path-style: true  # Required for MinIO

# Wasabi Cloud Storage
# dbs:
#   - path: /data/app.db
#     replicas:
#       - type: s3
#         endpoint: https://s3.wasabisys.com
#         bucket: my-wasabi-bucket
#         path: backups/app-db
#         region: us-east-1
#         access-key-id: ${WASABI_ACCESS_KEY}
#         secret-access-key: ${WASABI_SECRET_KEY}

# Backblaze B2
# dbs:
#   - path: /data/app.db
#     replicas:
#       - type: s3
#         endpoint: https://s3.us-west-002.backblazeb2.com
#         bucket: my-b2-bucket
#         path: litestream/app-db
#         region: us-west-002
#         access-key-id: ${B2_KEY_ID}
#         secret-access-key: ${B2_APPLICATION_KEY}

# DigitalOcean Spaces
# dbs:
#   - path: /data/app.db
#     replicas:
#       - type: s3
#         endpoint: https://nyc3.digitaloceanspaces.com
#         bucket: my-space
#         path: backups/app-db
#         region: nyc3
#         access-key-id: ${DO_SPACES_KEY}
#         secret-access-key: ${DO_SPACES_SECRET}
```

#### 3. Performance Tuning Examples

Create `etc/examples/performance-tuning.yml`:

```yaml
# Performance tuning examples for different scenarios

# Small database (<100MB) - Use defaults
dbs:
  - path: /data/small.db
    replicas:
      - type: s3
        bucket: backups
        path: small-db
        region: us-east-1
        # Defaults work well for small databases:
        # part-size: 5MB (default)
        # concurrency: 5 (default)

---
# Medium database (100MB - 1GB) - Balanced settings
dbs:
  - path: /data/medium.db
    replicas:
      - type: s3
        bucket: backups
        path: medium-db
        region: us-east-1
        part-size: 10MB      # Reduce number of parts
        concurrency: 5       # Moderate parallelism
        sync-interval: 30s   # Less frequent syncs

---
# Large database (1GB - 10GB) - Optimized for throughput
dbs:
  - path: /data/large.db
    replicas:
      - type: s3
        bucket: backups
        path: large-db
        region: us-east-1
        part-size: 20MB      # Larger parts
        concurrency: 10      # Higher parallelism
        http-timeout: 6h    # Longer timeout
        sync-interval: 5m    # Less frequent syncs

---
# Very large database (>10GB) - Maximum throughput
dbs:
  - path: /data/huge.db
    replicas:
      - type: s3
        bucket: backups
        path: huge-db
        region: us-east-1
        part-size: 50MB      # Large parts
        concurrency: 20      # Maximum parallelism
        http-timeout: 24h   # Very long timeout
        sync-interval: 1h    # Infrequent syncs

---
# Memory-constrained environment (container/VM)
dbs:
  - path: /data/app.db
    replicas:
      - type: s3
        bucket: backups
        path: app-db
        region: us-east-1
        part-size: 5MB       # Minimum size
        concurrency: 2       # Low parallelism
        # Total memory: ~10MB during uploads

---
# High-latency network (cross-region/continent)
dbs:
  - path: /data/app.db
    replicas:
      - type: s3
        bucket: backups
        path: app-db
        region: ap-southeast-1
        part-size: 20MB      # Fewer round trips
        concurrency: 10      # Overlap latency
        http-timeout: 12h   # Account for latency
```

#### 4. Authentication Examples

Create `etc/examples/authentication.yml`:

```yaml
# Various authentication methods for S3

# 1. Explicit credentials (not recommended for production)
dbs:
  - path: /data/app.db
    replicas:
      - type: s3
        bucket: my-bucket
        path: app-db
        region: us-east-1
        access-key-id: AKIAIOSFODNN7EXAMPLE
        secret-access-key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

---
# 2. Environment variables (recommended)
# Set these environment variables:
# AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
# AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# AWS_REGION=us-east-1
dbs:
  - path: /data/app.db
    replicas:
      - type: s3
        bucket: my-bucket
        path: app-db
        # Credentials loaded from environment

---
# 3. IAM Role (EC2/ECS/Lambda)
# No credentials needed when running on AWS infrastructure
dbs:
  - path: /data/app.db
    replicas:
      - type: s3
        bucket: my-bucket
        path: app-db
        region: us-east-1
        # IAM role credentials used automatically

---
# 4. Shared credentials file
# Uses ~/.aws/credentials and ~/.aws/config
dbs:
  - path: /data/app.db
    replicas:
      - type: s3
        bucket: my-bucket
        path: app-db
        # Credentials loaded from AWS config files

---
# 5. With encryption (age)
dbs:
  - path: /data/secure.db
    replicas:
      - type: s3
        bucket: encrypted-backups
        path: secure-db
        region: us-east-1
        age-public-keys:
          - age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
```

#### 5. Docker Compose Example

Create `etc/examples/docker-compose.yml`:

```yaml
version: '3.8'

services:
  app:
    image: myapp:latest
    volumes:
      - db-data:/data
    environment:
      - DATABASE_PATH=/data/app.db

  litestream:
    image: litestream/litestream:latest
    command: replicate
    volumes:
      - db-data:/data
      - ./litestream.yml:/etc/litestream.yml
    environment:
      # AWS credentials (or use IAM role on AWS)
      - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
      - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
      - AWS_REGION=us-east-1
    
    # Litestream configuration
    configs:
      - source: litestream_config
        target: /etc/litestream.yml

volumes:
  db-data:

configs:
  litestream_config:
    content: |
      dbs:
        - path: /data/app.db
          replicas:
            - type: s3
              bucket: my-app-backups
              path: docker-db
              region: us-east-1
              # Performance tuning for container environment
              part-size: 10MB
              concurrency: 3
              sync-interval: 30s
```

#### 6. Kubernetes ConfigMap Example

Create `etc/examples/kubernetes-configmap.yml`:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: litestream-config
  namespace: default
data:
  litestream.yml: |
    dbs:
      - path: /data/app.db
        replicas:
          - type: s3
            bucket: k8s-app-backups
            path: app-db
            region: us-east-1
            # Use IAM role for service account (IRSA) on EKS
            # No credentials needed
            
            # Performance settings for pod environment
            part-size: 10MB
            concurrency: 5
            http-timeout: 6h
            
            # Retention
            retention: 7d
            retention-check-interval: 1h
            
            # Sync settings
            sync-interval: 10s
            
    # Prometheus monitoring
    prometheus:
      url: http://prometheus:9090
      job: litestream
```

#### 7. Update README.md Examples

```markdown
## Quick Start

### Basic S3 Configuration

```yaml
dbs:
  - path: /path/to/db.sqlite
    replicas:
      - type: s3
        bucket: my-bucket
        path: db
        region: us-east-1
```

### With Performance Tuning (New in v0.4.0)

```yaml
dbs:
  - path: /path/to/large-db.sqlite
    replicas:
      - type: s3
        bucket: my-bucket
        path: db
        region: us-east-1
        part-size: 20MB      # Optimize for large files
        concurrency: 10      # Increase parallelism
```

### S3-Compatible Services

```yaml
# MinIO example
dbs:
  - path: /path/to/db.sqlite
    replicas:
      - type: s3
        endpoint: https://minio.example.com
        bucket: backups
        path: db
        force-path-style: true
```
```

### Implementation Steps

1. Update `etc/litestream.yml` with new options and comments
2. Create example files in `etc/examples/` directory
3. Update README.md with v0.4.0 examples
4. Test all example configurations
5. Add examples to documentation website
6. Create migration guide from old to new configurations

### Testing Requirements

- Validate all YAML files for syntax
- Test each configuration with actual Litestream binary
- Verify environment variable substitution works
- Test with various S3 and S3-compatible services
- Ensure examples work in Docker and Kubernetes

### Priority
Medium - Important for user adoption of new features

### Labels
- documentation
- examples
- configuration
- s3

Discussion:
@corylanou: ## Additional Example: NATS JetStream Object Store Configuration

With the NATS replica client being added in PR #712, here's an example configuration for NATS JetStream Object Store:

```yaml
# Example Litestream configuration file with NATS JetStream Object Store replica
#
# This configuration demonstrates how to use NATS JetStream Object Store as a
# replication destination for Litestream.
#
# NATS JetStream Object Store provides distributed, persistent object storage
# with features like:
# - Automatic chunking for large objects
# - Configurable replication and storage types
# - Built-in deduplication and compression
# - Multi-tenancy and security

# Database configuration
dbs:
  - path: /var/lib/db1.db
    replica:
      # NATS replica configuration
      type: nats

      # NATS server URL - can also be specified as "url: nats://server:port/bucket"
      url: nats://nats.example.com:4222

      # Object store bucket name (required)
      # IMPORTANT: Bucket must be pre-created using NATS CLI or API
      # Example: nats object add litestream-backups --max-bucket-size=10G --replicas=3
      bucket: litestream-backups

      # Authentication options (choose one method)

      # JWT authentication (recommended for production)
      jwt: /path/to/user.jwt
      seed: /path/to/user.nk

      # Or credentials file
      # creds: /path/to/user.creds

      # Or NKey authentication
      # nkey: /path/to/user.nk

      # Or username/password (not recommended for production)
      # username: myuser
      # password: mypassword

      # Or token authentication
      # token: mytoken

      # TLS configuration
      tls: true
      # root-cas:
      #   - /path/to/ca-cert.pem

      # Connection options (optional, defaults shown)
      max-reconnects: -1  # -1 for unlimited
      reconnect-wait: 2s
      timeout: 10s

      # Standard litestream replica options
      sync-interval: 1s

      # Age encryption (optional)
      age:
        identities:
          - /path/to/age-identity.txt
        recipients:
          - age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

# Multiple databases can be configured
  - path: /var/lib/db2.db
    replica:
      # Alternative URL format that includes bucket name
      url: nats://nats.example.com:4222/myapp-backups
      creds: /path/to/user.creds
```

### Key Points for NATS Configuration:
1. **Bucket must be pre-created** - Unlike some other storage backends, NATS buckets need to be created beforehand using the NATS CLI
2. **Multiple authentication methods** - Supports JWT, credentials file, NKey, username/password, and token auth
3. **JetStream must be enabled** - The NATS server must have JetStream enabled ()
4. **Object Store features** - Automatic chunking for large files, configurable replication

This should be added to the examples once PR #712 is merged.@corylanou: ## Additional Documentation: NATS Testing Guide

Here's the testing guide for the NATS replica client that should be included in the documentation:

### Testing NATS Replica Client

#### Prerequisites

1. **Install NATS server** (or use Docker):
```bash
# Using Docker
docker run -p 4222:4222 -p 8222:8222 nats:latest -js

# Or install locally
brew install nats-server  # macOS
# or download from https://github.com/nats-io/nats-server/releases
```

2. **Install NATS CLI tools**:
```bash
brew install nats-io/nats-tools/nats  # macOS
# or download from https://github.com/nats-io/natscli/releases
```

#### Manual Testing

##### 1. Start NATS Server with JetStream

```bash
# Start NATS with JetStream enabled
nats-server -js
```

##### 2. Create Object Store Bucket (Required)

The bucket must be pre-created before use:

```bash
# Create an object store bucket
nats object add litestream-backups --max-bucket-size=1G --replicas=3
```

##### 3. Configure Litestream

Create a test configuration file (`litestream-test.yml`):

```yaml
dbs:
  - path: /tmp/test.db
    replica:
      type: nats
      url: nats://localhost:4222
      bucket: litestream-backups
      # Optional: Use authentication
      # username: myuser
      # password: mypassword
```

##### 4. Run Litestream

```bash
# Create a test database
sqlite3 /tmp/test.db "CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT);"
sqlite3 /tmp/test.db "INSERT INTO test (value) VALUES ('test data');"

# Run litestream with the test config
./litestream replicate -config litestream-test.yml
```

##### 5. Verify Replication

```bash
# List objects in the bucket
nats object ls litestream-backups

# Get info about the bucket
nats object info litestream-backups
```

##### 6. Test Restoration

```bash
# Stop litestream
# Delete the original database
rm /tmp/test.db

# Restore from NATS
./litestream restore -config litestream-test.yml /tmp/test.db

# Verify the data
sqlite3 /tmp/test.db "SELECT * FROM test;"
```

#### Integration Testing

Run the integration tests with a real NATS server:

```bash
# Start NATS server with JetStream
docker run -d -p 4222:4222 --name nats-test nats:latest -js

# Run integration tests
go test -v ./replica_client_test.go -integration nats

# Clean up
docker stop nats-test
docker rm nats-test
```

#### Authentication Testing

##### JWT/NKey Authentication

1. Generate credentials:
```bash
# Create an operator
nsc add operator TestOperator

# Create an account
nsc add account TestAccount

# Create a user
nsc add user TestUser

# Export the credentials
nsc generate creds -n TestUser > user.creds
```

2. Configure Litestream:
```yaml
dbs:
  - path: /tmp/test.db
    replica:
      type: nats
      url: nats://localhost:4222
      bucket: litestream-backups
      creds: /path/to/user.creds
```

##### Username/Password Authentication

1. Create a NATS server config with auth:
```conf
# nats-auth.conf
jetstream: enabled
authorization {
  users = [
    {user: testuser, password: testpass}
  ]
}
```

2. Start NATS with config:
```bash
nats-server -c nats-auth.conf
```

3. Configure Litestream:
```yaml
dbs:
  - path: /tmp/test.db
    replica:
      type: nats
      url: nats://localhost:4222
      bucket: litestream-backups
      username: testuser
      password: testpass
```

#### Performance Testing

For performance testing with larger databases:

```bash
# Create a larger test database
sqlite3 /tmp/large.db < large-dataset.sql

# Run litestream with metrics enabled
./litestream replicate -config litestream-test.yml -addr :9090

# Monitor metrics at http://localhost:9090/metrics
```

#### Troubleshooting

1. **Connection Issues**: Ensure NATS server is running and JetStream is enabled
2. **Authentication Failures**: Verify credentials and permissions
3. **Bucket Issues**: Check bucket exists and has sufficient space/replicas
4. **Performance**: Monitor NATS server metrics and adjust bucket configuration as needed

#### Important Notes

- The NATS replica client uses the LTX format for storing database snapshots and WAL segments
- Object Store provides automatic chunking for large files
- Bucket replication is handled by NATS JetStream, not Litestream
- TTL and retention should generally be managed by Litestream, not NATS bucket settings
- **Buckets must be pre-created** - the NATS replica client does not auto-create buckets

This documentation should be added to the main Litestream documentation once PR #712 is merged.@corylanou: This issue has been transferred to the litestream.io repository where documentation issues are now tracked: https://github.com/benbjohnson/litestream.io/issues/97@corylanou: Transferred to litestream.io repository for documentation tracking.

Solution patch:
diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml
index 61c5457..b39071e 100644
--- a/.github/workflows/commit.yml
+++ b/.github/workflows/commit.yml
@@ -1,5 +1,7 @@
 on:
   push:
+    branches:
+      - main
   pull_request:
     types:
       - opened
@@ -102,6 +104,65 @@ jobs:
 
       - run: ./etc/s3_mock.py go test -v ./replica_client_test.go -integration s3
 
+  nats-docker-test:
+    name: Run NATS Integration Tests
+    runs-on: ubuntu-latest
+    needs: build
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: actions/setup-go@v5
+        with:
+          go-version-file: "go.mod"
+
+      - name: Start NATS Server with JetStream
+        run: |
+          docker run -d \
+            --name nats-test \
+            -p 4222:4222 \
+            -p 8222:8222 \
+            nats:latest \
+            -js \
+            -DV
+
+          # Wait for NATS to be ready
+          echo "Waiting for NATS server to be ready..."
+          for i in {1..30}; do
+            if nc -z localhost 4222; then
+              echo "NATS server is ready"
+              break
+            fi
+            echo "Waiting for NATS server... ($i/30)"
+            sleep 1
+          done
+
+      - name: Create NATS Object Store Bucket
+        run: |
+          # Install NATS CLI - get latest version
+          NATS_VERSION=$(curl -s https://api.github.com/repos/nats-io/natscli/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
+          wget "https://github.com/nats-io/natscli/releases/download/v${NATS_VERSION}/nats-${NATS_VERSION}-linux-amd64.zip" -O nats.zip
+          unzip nats.zip
+          sudo mv "nats-${NATS_VERSION}-linux-amd64/nats" /usr/local/bin/
+          sudo chmod +x /usr/local/bin/nats
+
+          # Create the object store bucket
+          nats object add litestream-test --max-bucket-size=100M --replicas=1
+
+      - run: go env
+
+      - run: go install ./cmd/litestream
+
+      - run: go test -v ./replica_client_test.go -integration nats
+        env:
+          LITESTREAM_NATS_URL: "nats://localhost:4222"
+          LITESTREAM_NATS_BUCKET: "litestream-test"
+
+      - name: Cleanup
+        if: always()
+        run: |
+          docker stop nats-test || true
+          docker rm nats-test || true
+
   s3-integration-test:
     name: Run S3 Integration Tests
     runs-on: ubuntu-latest
diff --git a/go.mod b/go.mod
index 059f7c8..1e78f01 100644
--- a/go.mod
+++ b/go.mod
@@ -20,6 +20,7 @@ require (
 	github.com/mark3labs/mcp-go v0.32.0
 	github.com/mattn/go-shellwords v1.0.12
 	github.com/mattn/go-sqlite3 v1.14.19
+	github.com/nats-io/nats.go v1.44.0
 	github.com/pkg/sftp v1.13.6
 	github.com/prometheus/client_golang v1.17.0
 	github.com/superfly/ltx v0.3.18
@@ -63,11 +64,14 @@ require (
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
 	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
+	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/kr/fs v0.1.0 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.17 // indirect
 	github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
+	github.com/nats-io/nkeys v0.4.11 // indirect
+	github.com/nats-io/nuid v1.0.1 // indirect
 	github.com/pierrec/lz4/v4 v4.1.22 // indirect
 	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
 	github.com/prometheus/client_model v0.5.0 // indirect
diff --git a/go.sum b/go.sum
index 08d33c2..23adcf9 100644
--- a/go.sum
+++ b/go.sum
@@ -143,6 +143,8 @@ github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfk
 github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
 github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
 github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -167,6 +169,12 @@ github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbW
 github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
+github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
+github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
+github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
+github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
+github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
 github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
diff --git a/nats/replica_client.go b/nats/replica_client.go
new file mode 100644
index 0000000..03464d1
--- /dev/null
+++ b/nats/replica_client.go
@@ -0,0 +1,424 @@
+package nats
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/nats-io/nats.go"
+	"github.com/nats-io/nats.go/jetstream"
+	"github.com/superfly/ltx"
+
+	"github.com/benbjohnson/litestream"
+	"github.com/benbjohnson/litestream/internal"
+)
+
+// ReplicaClientType is the client type for this package.
+const ReplicaClientType = "nats"
+
+var _ litestream.ReplicaClient = (*ReplicaClient)(nil)
+
+// ReplicaClient is a client for writing LTX files to NATS JetStream Object Store.
+type ReplicaClient struct {
+	mu sync.Mutex
+
+	// NATS connection and JetStream context
+	nc          *nats.Conn
+	js          jetstream.JetStream
+	objectStore jetstream.ObjectStore
+
+	// Configuration
+	URL        string   // NATS server URL
+	BucketName string   // Object store bucket name
+	Path       string   // Base path for LTX files within the bucket
+	JWT        string   // JWT token for authentication
+	Seed       string   // Seed for JWT authentication
+	Creds      string   // Credentials file path
+	NKey       string   // NKey for authentication
+	Username   string   // Username for authentication
+	Password   string   // Password for authentication
+	Token      string   // Token for authentication
+	TLS        bool     // Enable TLS
+	RootCAs    []string // Root CA certificates
+
+	// Note: Bucket configuration (replicas, storage, TTL, etc.) should be
+	// managed externally via NATS CLI or API, not by Litestream
+
+	// Connection options
+	MaxReconnects    int                          // Maximum reconnection attempts (-1 for unlimited)
+	ReconnectWait    time.Duration                // Wait time between reconnection attempts
+	ReconnectJitter  time.Duration                // Random jitter for reconnection
+	Timeout          time.Duration                // Connection timeout
+	PingInterval     time.Duration                // Ping interval
+	MaxPingsOut      int                          // Maximum number of pings without response
+	ReconnectBufSize int                          // Reconnection buffer size
+	UserJWT          func() (string, error)       // JWT callback
+	SigCB            func([]byte) ([]byte, error) // Signature callback
+}
+
+// NewReplicaClient returns a new instance of ReplicaClient.
+func NewReplicaClient() *ReplicaClient {
+	return &ReplicaClient{
+		MaxReconnects:    -1, // Unlimited
+		ReconnectWait:    2 * time.Second,
+		Timeout:          10 * time.Second,
+		PingInterval:     2 * time.Minute,
+		MaxPingsOut:      2,
+		ReconnectBufSize: 8 * 1024 * 1024, // 8MB
+	}
+}
+
+// Type returns "nats" as the client type.
+func (c *ReplicaClient) Type() string {
+	return ReplicaClientType
+}
+
+// Init initializes the connection to NATS JetStream. No-op if already initialized.
+func (c *ReplicaClient) Init(ctx context.Context) error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	if c.nc != nil {
+		return nil
+	}
+
+	if err := c.connect(ctx); err != nil {
+		return fmt.Errorf("nats: failed to connect: %w", err)
+	}
+
+	if err := c.initObjectStore(ctx); err != nil {
+		return fmt.Errorf("nats: failed to initialize object store: %w", err)
+	}
+
+	return nil
+}
+
+// connect establishes a connection to NATS server with proper configuration.
+func (c *ReplicaClient) connect(_ context.Context) error {
+	opts := []nats.Option{
+		nats.MaxReconnects(c.MaxReconnects),
+		nats.ReconnectWait(c.ReconnectWait),
+		nats.ReconnectJitter(c.ReconnectJitter, c.ReconnectJitter*2),
+		nats.Timeout(c.Timeout),
+		nats.PingInterval(c.PingInterval),
+		nats.MaxPingsOutstanding(c.MaxPingsOut),
+		nats.ReconnectBufSize(c.ReconnectBufSize),
+	}
+
+	// Authentication options
+	if c.JWT != "" && c.Seed != "" {
+		opts = append(opts, nats.UserJWTAndSeed(c.JWT, c.Seed))
+	} else if c.Creds != "" {
+		opts = append(opts, nats.UserCredentials(c.Creds))
+	} else if c.NKey != "" {
+		opts = append(opts, nats.Nkey(c.NKey, c.SigCB))
+	} else if c.Username != "" && c.Password != "" {
+		opts = append(opts, nats.UserInfo(c.Username, c.Password))
+	} else if c.Token != "" {
+		opts = append(opts, nats.Token(c.Token))
+	}
+
+	// JWT callback
+	if c.UserJWT != nil {
+		opts = append(opts, nats.UserJWT(c.UserJWT, c.SigCB))
+	}
+
+	// TLS configuration
+	if c.TLS {
+		opts = append(opts, nats.Secure())
+		if len(c.RootCAs) > 0 {
+			// Note: Root CA configuration would need additional setup
+			// This is a simplified version - real implementation may need more setup
+		}
+	}
+
+	// Note: NATS Connect doesn't directly support context cancellation during connection
+	// The context parameter is preserved for potential future use
+
+	url := c.URL
+	if url == "" {
+		url = nats.DefaultURL
+	}
+
+	nc, err := nats.Connect(url, opts...)
+	if err != nil {
+		return fmt.Errorf("failed to connect to NATS server: %w", err)
+	}
+
+	js, err := jetstream.New(nc)
+	if err != nil {
+		nc.Close()
+		return fmt.Errorf("failed to create JetStream context: %w", err)
+	}
+
+	c.nc = nc
+	c.js = js
+	return nil
+}
+
+// initObjectStore retrieves the existing object store bucket.
+// The bucket must be pre-created using the NATS CLI or API.
+func (c *ReplicaClient) initObjectStore(ctx context.Context) error {
+	if c.BucketName == "" {
+		return fmt.Errorf("bucket name is required")
+	}
+
+	// Get existing object store - do not auto-create
+	objectStore, err := c.js.ObjectStore(ctx, c.BucketName)
+	if err != nil {
+		return fmt.Errorf("failed to access object store bucket %q (bucket must be created beforehand): %w", c.BucketName, err)
+	}
+
+	c.objectStore = objectStore
+	return nil
+}
+
+// Close closes the NATS connection.
+func (c *ReplicaClient) Close() error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	if c.nc != nil {
+		c.nc.Close()
+		c.nc = nil
+		c.js = nil
+		c.objectStore = nil
+	}
+	return nil
+}
+
+// ltxPath returns the object path for an LTX file.
+func (c *ReplicaClient) ltxPath(level int, minTXID, maxTXID ltx.TXID) string {
+	return litestream.LTXFilePath(c.Path, level, minTXID, maxTXID)
+}
+
+// parseLTXPath parses an LTX object path and returns level, minTXID, and maxTXID.
+func (c *ReplicaClient) parseLTXPath(objPath string) (level int, minTXID, maxTXID ltx.TXID, err error) {
+	// Remove the base path prefix if present
+	if c.Path != "" && strings.HasPrefix(objPath, c.Path+"/") {
+		objPath = strings.TrimPrefix(objPath, c.Path+"/")
+	}
+
+	// Expected format: "ltx/<level>/<minTXID>-<maxTXID>.ltx"
+	parts := strings.Split(objPath, "/")
+	if len(parts) < 3 || parts[0] != "ltx" {
+		return 0, 0, 0, fmt.Errorf("invalid ltx path: %s", objPath)
+	}
+
+	// Parse level
+	if level, err = strconv.Atoi(parts[1]); err != nil {
+		return 0, 0, 0, fmt.Errorf("invalid level in path %s: %w", objPath, err)
+	}
+
+	// Parse filename (minTXID-maxTXID.ltx)
+	filename := parts[2]
+	minTXIDVal, maxTXIDVal, err := ltx.ParseFilename(filename)
+	if err != nil {
+		return 0, 0, 0, fmt.Errorf("invalid filename in path %s: %w", objPath, err)
+	}
+
+	return level, minTXIDVal, maxTXIDVal, nil
+}
+
+// LTXFiles returns an iterator of all LTX files on the replica for a given level.
+func (c *ReplicaClient) LTXFiles(ctx context.Context, level int, seek ltx.TXID) (ltx.FileIterator, error) {
+	if err := c.Init(ctx); err != nil {
+		return nil, err
+	}
+
+	// List all objects in the store
+	internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "LIST").Inc()
+	objectList, err := c.objectStore.List(ctx)
+	if err != nil {
+		// NATS returns "no objects found" when bucket is empty, treat as empty list
+		if strings.Contains(err.Error(), "no objects found") {
+			objectList = nil // Empty list
+		} else {
+			return nil, fmt.Errorf("failed to list objects: %w", err)
+		}
+	}
+
+	var fileInfos []*ltx.FileInfo
+	prefix := litestream.LTXLevelDir(c.Path, level) + "/"
+
+	for _, objInfo := range objectList {
+		// Filter by level prefix
+		if !strings.HasPrefix(objInfo.Name, prefix) {
+			continue
+		}
+
+		fileLevel, minTXID, maxTXID, err := c.parseLTXPath(objInfo.Name)
+		if err != nil {
+			continue // Skip invalid paths
+		}
+
+		if fileLevel != level {
+			continue
+		}
+
+		// Apply seek filter
+		if minTXID < seek {
+			continue
+		}
+
+		fileInfos = append(fileInfos, &ltx.FileInfo{
+			Level:   fileLevel,
+			MinTXID: minTXID,
+			MaxTXID: maxTXID,
+			Size:    int64(objInfo.Size),
+		})
+	}
+
+	// Sort by minTXID
+	sort.Slice(fileInfos, func(i, j int) bool {
+		return fileInfos[i].MinTXID < fileInfos[j].MinTXID
+	})
+
+	return &ltxFileIterator{files: fileInfos, index: -1}, nil
+}
+
+// OpenLTXFile returns a reader that contains an LTX file at a given TXID range.
+func (c *ReplicaClient) OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID) (io.ReadCloser, error) {
+	if err := c.Init(ctx); err != nil {
+		return nil, err
+	}
+
+	objectPath := c.ltxPath(level, minTXID, maxTXID)
+
+	objectResult, err := c.objectStore.Get(ctx, objectPath)
+	if err != nil {
+		if isNotFoundError(err) {
+			return nil, os.ErrNotExist
+		}
+		return nil, fmt.Errorf("failed to get object %s: %w", objectPath, err)
+	}
+
+	// Record metrics
+	internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "GET").Inc()
+	// Note: We can't get the size from NATS object reader directly, so we skip bytes counter
+
+	return objectResult, nil
+}
+
+// WriteLTXFile writes an LTX file to the replica.
+func (c *ReplicaClient) WriteLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, r io.Reader) (*ltx.FileInfo, error) {
+	if err := c.Init(ctx); err != nil {
+		return nil, err
+	}
+
+	objectPath := c.ltxPath(level, minTXID, maxTXID)
+	startTime := time.Now()
+
+	// Wrap reader to count bytes
+	rc := internal.NewReadCounter(r)
+
+	objectInfo, err := c.objectStore.Put(ctx, jetstream.ObjectMeta{
+		Name: objectPath,
+	}, rc)
+	if err != nil {
+		return nil, fmt.Errorf("failed to put object %s: %w", objectPath, err)
+	}
+
+	// Record metrics
+	internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "PUT").Inc()
+	internal.OperationBytesCounterVec.WithLabelValues(ReplicaClientType, "PUT").Add(float64(objectInfo.Size))
+
+	return &ltx.FileInfo{
+		Level:     level,
+		MinTXID:   minTXID,
+		MaxTXID:   maxTXID,
+		Size:      int64(objectInfo.Size),
+		CreatedAt: startTime.UTC(),
+	}, nil
+}
+
+// DeleteLTXFiles deletes one or more LTX files.
+func (c *ReplicaClient) DeleteLTXFiles(ctx context.Context, a []*ltx.FileInfo) error {
+	if err := c.Init(ctx); err != nil {
+		return err
+	}
+
+	for _, fileInfo := range a {
+		objectPath := c.ltxPath(fileInfo.Level, fileInfo.MinTXID, fileInfo.MaxTXID)
+
+		if err := c.objectStore.Delete(ctx, objectPath); err != nil {
+			if !isNotFoundError(err) {
+				return fmt.Errorf("failed to delete object %s: %w", objectPath, err)
+			}
+		}
+		internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "DELETE").Inc()
+	}
+
+	return nil
+}
+
+// DeleteAll deletes all files in the object store.
+func (c *ReplicaClient) DeleteAll(ctx context.Context) error {
+	if err := c.Init(ctx); err != nil {
+		return err
+	}
+
+	// List all objects in the bucket
+	objectList, err := c.objectStore.List(ctx)
+	if err != nil {
+		// NATS returns "no objects found" when bucket is empty, treat as empty list
+		if strings.Contains(err.Error(), "no objects found") {
+			objectList = nil // Empty list, nothing to delete
+		} else {
+			return fmt.Errorf("failed to list all objects: %w", err)
+		}
+	}
+
+	for _, objInfo := range objectList {
+		if err := c.objectStore.Delete(ctx, objInfo.Name); err != nil {
+			if !isNotFoundError(err) {
+				return fmt.Errorf("failed to delete object %s: %w", objInfo.Name, err)
+			}
+		}
+		internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "DELETE").Inc()
+	}
+
+	return nil
+}
+
+// isNotFoundError checks if the error is a "not found" error.
+func isNotFoundError(err error) bool {
+	return err != nil && (err == jetstream.ErrObjectNotFound || strings.Contains(err.Error(), "not found"))
+}
+
+// ltxFileIterator implements ltx.FileIterator for NATS object store.
+type ltxFileIterator struct {
+	files []*ltx.FileInfo
+	index int
+	err   error
+}
+
+// Next advances the iterator to the next file.
+func (itr *ltxFileIterator) Next() bool {
+	itr.index++
+	return itr.index < len(itr.files)
+}
+
+// Item returns the current file info.
+func (itr *ltxFileIterator) Item() *ltx.FileInfo {
+	if itr.index < 0 || itr.index >= len(itr.files) {
+		return nil
+	}
+	return itr.files[itr.index]
+}
+
+// Err returns any error that occurred during iteration.
+func (itr *ltxFileIterator) Err() error {
+	return itr.err
+}
+
+// Close closes the iterator.
+func (itr *ltxFileIterator) Close() error {
+	return nil
+}



Test patch:
diff --git a/cmd/litestream/main.go b/cmd/litestream/main.go
index eb97676..de91d8c 100644
--- a/cmd/litestream/main.go
+++ b/cmd/litestream/main.go
@@ -26,6 +26,7 @@ import (
 	"github.com/benbjohnson/litestream/file"
 	"github.com/benbjohnson/litestream/gs"
 	"github.com/benbjohnson/litestream/internal"
+	"github.com/benbjohnson/litestream/nats"
 	"github.com/benbjohnson/litestream/s3"
 	"github.com/benbjohnson/litestream/sftp"
 )
@@ -559,6 +560,19 @@ type ReplicaConfig struct {
 	Password string `yaml:"password"`
 	KeyPath  string `yaml:"key-path"`
 
+	// NATS settings
+	JWT           string         `yaml:"jwt"`
+	Seed          string         `yaml:"seed"`
+	Creds         string         `yaml:"creds"`
+	NKey          string         `yaml:"nkey"`
+	Username      string         `yaml:"username"`
+	Token         string         `yaml:"token"`
+	TLS           bool           `yaml:"tls"`
+	RootCAs       []string       `yaml:"root-cas"`
+	MaxReconnects *int           `yaml:"max-reconnects"`
+	ReconnectWait *time.Duration `yaml:"reconnect-wait"`
+	Timeout       *time.Duration `yaml:"timeout"`
+
 	// Encryption identities and recipients
 	Age struct {
 		Identities []string `yaml:"identities"`
@@ -617,6 +631,10 @@ func NewReplicaFromConfig(c *ReplicaConfig, db *litestream.DB) (_ *litestream.Re
 		if r.Client, err = newSFTPReplicaClientFromConfig(c, r); err != nil {
 			return nil, err
 		}
+	case "nats":
+		if r.Client, err = newNATSReplicaClientFromConfig(c, r); err != nil {
+			return nil, err
+		}
 	default:
 		return nil, fmt.Errorf("unknown replica type in config: %q", c.Type)
 	}
@@ -850,6 +868,72 @@ func newSFTPReplicaClientFromConfig(c *ReplicaConfig, _ *litestream.Replica) (_
 	return client, nil
 }
 
+// newNATSReplicaClientFromConfig returns a new instance of nats.ReplicaClient built from config.
+func newNATSReplicaClientFromConfig(c *ReplicaConfig, _ *litestream.Replica) (_ *nats.ReplicaClient, err error) {
+	// Parse URL if provided to extract bucket name and server URL
+	var url, bucket string
+	if c.URL != "" {
+		scheme, host, path, err := ParseReplicaURL(c.URL)
+		if err != nil {
+			return nil, fmt.Errorf("invalid NATS URL: %w", err)
+		}
+		if scheme != "nats" {
+			return nil, fmt.Errorf("invalid scheme for NATS replica: %s", scheme)
+		}
+
+		// Reconstruct URL without bucket path
+		if host != "" {
+			url = fmt.Sprintf("nats://%s", host)
+		}
+
+		// Extract bucket name from path
+		if path != "" {
+			bucket = strings.Trim(path, "/")
+		}
+	}
+
+	// Use bucket from config if not extracted from URL
+	if bucket == "" {
+		bucket = c.Bucket
+	}
+
+	// Ensure required settings are set
+	if bucket == "" {
+		return nil, fmt.Errorf("bucket required for NATS replica")
+	}
+
+	// Build replica client
+	client := nats.NewReplicaClient()
+	client.URL = url
+	client.BucketName = bucket
+
+	// Set authentication options
+	client.JWT = c.JWT
+	client.Seed = c.Seed
+	client.Creds = c.Creds
+	client.NKey = c.NKey
+	client.Username = c.Username
+	client.Password = c.Password
+	client.Token = c.Token
+
+	// Set TLS options
+	client.TLS = c.TLS
+	client.RootCAs = c.RootCAs
+
+	// Set connection options with defaults
+	if c.MaxReconnects != nil {
+		client.MaxReconnects = *c.MaxReconnects
+	}
+	if c.ReconnectWait != nil {
+		client.ReconnectWait = *c.ReconnectWait
+	}
+	if c.Timeout != nil {
+		client.Timeout = *c.Timeout
+	}
+
+	return client, nil
+}
+
 // applyLitestreamEnv copies "LITESTREAM" prefixed environment variables to
 // their AWS counterparts as the "AWS" prefix can be confusing when using a
 // non-AWS S3-compatible service.
diff --git a/cmd/litestream/replicate.go b/cmd/litestream/replicate.go
index ea5d9a8..21c9582 100644
--- a/cmd/litestream/replicate.go
+++ b/cmd/litestream/replicate.go
@@ -18,6 +18,7 @@ import (
 	"github.com/benbjohnson/litestream/abs"
 	"github.com/benbjohnson/litestream/file"
 	"github.com/benbjohnson/litestream/gs"
+	"github.com/benbjohnson/litestream/nats"
 	"github.com/benbjohnson/litestream/s3"
 	"github.com/benbjohnson/litestream/sftp"
 )
@@ -148,6 +149,8 @@ func (c *ReplicateCommand) Run() (err error) {
 			slog.Info("replicating to", "bucket", client.Bucket, "path", client.Path, "endpoint", client.Endpoint)
 		case *sftp.ReplicaClient:
 			slog.Info("replicating to", "host", client.Host, "user", client.User, "path", client.Path)
+		case *nats.ReplicaClient:
+			slog.Info("replicating to", "bucket", client.BucketName, "url", client.URL)
 		default:
 			slog.Info("replicating to")
 		}
diff --git a/nats/replica_client_test.go b/nats/replica_client_test.go
new file mode 100644
index 0000000..91df857
--- /dev/null
+++ b/nats/replica_client_test.go
@@ -0,0 +1,213 @@
+package nats
+
+import (
+	"testing"
+	"time"
+
+	"github.com/superfly/ltx"
+)
+
+func TestReplicaClient_Type(t *testing.T) {
+	client := NewReplicaClient()
+	if got, want := client.Type(), "nats"; got != want {
+		t.Fatalf("Type()=%s, want %s", got, want)
+	}
+}
+
+func TestReplicaClient_ltxPath(t *testing.T) {
+	client := NewReplicaClient()
+
+	tests := []struct {
+		level   int
+		minTXID ltx.TXID
+		maxTXID ltx.TXID
+		want    string
+	}{
+		{0, ltx.TXID(0x1000), ltx.TXID(0x2000), "ltx/0/0000000000001000-0000000000002000.ltx"},
+		{1, ltx.TXID(0xabcd), ltx.TXID(0xef01), "ltx/1/000000000000abcd-000000000000ef01.ltx"},
+		{255, ltx.TXID(0xffffffffffffffff), ltx.TXID(0xffffffffffffffff), "ltx/255/ffffffffffffffff-ffffffffffffffff.ltx"},
+	}
+
+	for _, test := range tests {
+		if got := client.ltxPath(test.level, test.minTXID, test.maxTXID); got != test.want {
+			t.Errorf("ltxPath(%d, %x, %x)=%s, want %s", test.level, test.minTXID, test.maxTXID, got, test.want)
+		}
+	}
+}
+
+func TestReplicaClient_parseLTXPath(t *testing.T) {
+	client := NewReplicaClient()
+
+	tests := []struct {
+		path    string
+		level   int
+		minTXID ltx.TXID
+		maxTXID ltx.TXID
+		wantErr bool
+	}{
+		{
+			path:    "ltx/0/0000000000001000-0000000000002000.ltx",
+			level:   0,
+			minTXID: ltx.TXID(0x1000),
+			maxTXID: ltx.TXID(0x2000),
+			wantErr: false,
+		},
+		{
+			path:    "ltx/1/000000000000abcd-000000000000ef01.ltx",
+			level:   1,
+			minTXID: ltx.TXID(0xabcd),
+			maxTXID: ltx.TXID(0xef01),
+			wantErr: false,
+		},
+		{
+			path:    "invalid/path",
+			wantErr: true,
+		},
+		{
+			path:    "ltx/x/invalid-invalid.ltx",
+			wantErr: true,
+		},
+		{
+			path:    "ltx/0/invalid.ltx",
+			wantErr: true,
+		},
+	}
+
+	for _, test := range tests {
+		level, minTXID, maxTXID, err := client.parseLTXPath(test.path)
+		if test.wantErr {
+			if err == nil {
+				t.Errorf("parseLTXPath(%s) expected error, got nil", test.path)
+			}
+			continue
+		}
+
+		if err != nil {
+			t.Errorf("parseLTXPath(%s) unexpected error: %v", test.path, err)
+			continue
+		}
+
+		if level != test.level {
+			t.Errorf("parseLTXPath(%s) level=%d, want %d", test.path, level, test.level)
+		}
+		if minTXID != test.minTXID {
+			t.Errorf("parseLTXPath(%s) minTXID=%x, want %x", test.path, minTXID, test.minTXID)
+		}
+		if maxTXID != test.maxTXID {
+			t.Errorf("parseLTXPath(%s) maxTXID=%x, want %x", test.path, maxTXID, test.maxTXID)
+		}
+	}
+}
+
+func TestReplicaClient_isNotFoundError(t *testing.T) {
+	tests := []struct {
+		name string
+		err  error
+		want bool
+	}{
+		{
+			name: "nil error",
+			err:  nil,
+			want: false,
+		},
+		{
+			name: "not found error",
+			err:  &mockNotFoundError{},
+			want: true,
+		},
+		{
+			name: "other error",
+			err:  &mockOtherError{},
+			want: false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if got := isNotFoundError(test.err); got != test.want {
+				t.Errorf("isNotFoundError() = %v, want %v", got, test.want)
+			}
+		})
+	}
+}
+
+func TestLtxFileIterator(t *testing.T) {
+	files := []*ltx.FileInfo{
+		{Level: 0, MinTXID: 1, MaxTXID: 10, Size: 100},
+		{Level: 0, MinTXID: 11, MaxTXID: 20, Size: 200},
+		{Level: 0, MinTXID: 21, MaxTXID: 30, Size: 300},
+	}
+
+	itr := &ltxFileIterator{files: files, index: -1}
+
+	// Test initial state
+	if item := itr.Item(); item != nil {
+		t.Errorf("Item() before Next() should return nil, got %v", item)
+	}
+
+	// Test iteration
+	var items []*ltx.FileInfo
+	for itr.Next() {
+		item := itr.Item()
+		if item == nil {
+			t.Fatal("Item() returned nil during valid iteration")
+		}
+		items = append(items, item)
+	}
+
+	if len(items) != len(files) {
+		t.Errorf("Expected %d items, got %d", len(files), len(items))
+	}
+
+	for i, item := range items {
+		if item != files[i] {
+			t.Errorf("Item %d: expected %v, got %v", i, files[i], item)
+		}
+	}
+
+	// Test after iteration ends
+	if itr.Next() {
+		t.Error("Next() should return false after iteration ends")
+	}
+
+	// Test Close
+	if err := itr.Close(); err != nil {
+		t.Errorf("Close() returned error: %v", err)
+	}
+
+	// Test Err
+	if err := itr.Err(); err != nil {
+		t.Errorf("Err() returned error: %v", err)
+	}
+}
+
+// Mock error types for testing
+
+type mockNotFoundError struct{}
+
+func (e *mockNotFoundError) Error() string {
+	return "not found"
+}
+
+type mockOtherError struct{}
+
+func (e *mockOtherError) Error() string {
+	return "some other error"
+}
+
+func TestReplicaClientDefaults(t *testing.T) {
+	client := NewReplicaClient()
+
+	// Test default values
+	if client.MaxReconnects != -1 {
+		t.Errorf("Expected MaxReconnects=-1, got %d", client.MaxReconnects)
+	}
+
+	if client.ReconnectWait != 2*time.Second {
+		t.Errorf("Expected ReconnectWait=2s, got %v", client.ReconnectWait)
+	}
+
+	if client.Timeout != 10*time.Second {
+		t.Errorf("Expected Timeout=10s, got %v", client.Timeout)
+	}
+}
diff --git a/replica_client_test.go b/replica_client_test.go
index 453ef13..32c7521 100644
--- a/replica_client_test.go
+++ b/replica_client_test.go
@@ -19,6 +19,7 @@ import (
 	"github.com/benbjohnson/litestream/abs"
 	"github.com/benbjohnson/litestream/file"
 	"github.com/benbjohnson/litestream/gs"
+	"github.com/benbjohnson/litestream/nats"
 	"github.com/benbjohnson/litestream/s3"
 	"github.com/benbjohnson/litestream/sftp"
 )
@@ -64,6 +65,15 @@ var (
 	sftpPath     = flag.String("sftp-path", os.Getenv("LITESTREAM_SFTP_PATH"), "")
 )
 
+// NATS settings
+var (
+	natsURL      = flag.String("nats-url", os.Getenv("LITESTREAM_NATS_URL"), "")
+	natsBucket   = flag.String("nats-bucket", os.Getenv("LITESTREAM_NATS_BUCKET"), "")
+	natsCreds    = flag.String("nats-creds", os.Getenv("LITESTREAM_NATS_CREDS"), "")
+	natsUsername = flag.String("nats-username", os.Getenv("LITESTREAM_NATS_USERNAME"), "")
+	natsPassword = flag.String("nats-password", os.Getenv("LITESTREAM_NATS_PASSWORD"), "")
+)
+
 func TestReplicaClient_LTX(t *testing.T) {
 	RunWithReplicaClient(t, "OK", func(t *testing.T, c litestream.ReplicaClient) {
 		t.Parallel()
@@ -243,6 +253,8 @@ func NewReplicaClient(tb testing.TB, typ string) litestream.ReplicaClient {
 		return NewABSReplicaClient(tb)
 	case sftp.ReplicaClientType:
 		return NewSFTPReplicaClient(tb)
+	case nats.ReplicaClientType:
+		return NewNATSReplicaClient(tb)
 	default:
 		tb.Fatalf("invalid replica client type: %q", typ)
 		return nil
@@ -306,6 +318,19 @@ func NewSFTPReplicaClient(tb testing.TB) *sftp.ReplicaClient {
 	return c
 }
 
+// NewNATSReplicaClient returns a new client for integration testing.
+func NewNATSReplicaClient(tb testing.TB) *nats.ReplicaClient {
+	tb.Helper()
+
+	c := nats.NewReplicaClient()
+	c.URL = *natsURL
+	c.BucketName = *natsBucket
+	c.Creds = *natsCreds
+	c.Username = *natsUsername
+	c.Password = *natsPassword
+	return c
+}
+
 // MustDeleteAll deletes all objects under the client's path.
 func MustDeleteAll(tb testing.TB, c litestream.ReplicaClient) {
 	tb.Helper()