Skip to content

Storage Configuration

Baan stores article bodies, images, uploaded media, and generated files such as llms.txt in a pluggable storage backend. SQLite/libSQL stores operational data such as settings, users, sessions, article metadata, the article index, API keys, and rate-limit counters.

You can switch between storage providers from the admin panel without rebuilding the app. The System settings flow can copy existing content to the new provider before Baan starts using it.

| Provider | Environments | Best For | |----------|-------------|---------| | local | Self-hosted only | Simple setups, development, single-server deployments (local filesystem) | | gcs | All | Google Cloud users; integrates well with GKE, Cloud Run | | s3 | All | AWS users; widest ecosystem and tooling | | r2 | All | Cost-conscious teams; free egress from Cloudflare’s network |

  1. Go to Admin → Settings → System
  2. Select the new storage provider and enter credentials
  3. Click Migrate to copy existing content to the new location
  4. Save — Baan starts using the selected provider after the migration/switch completes

No rebuild is required. Runtime switching is handled through the provider value saved in the database.


Files are saved to the content/ directory in the project root.

No environment variables needed. The provider is selected during setup and stored in the database.

When using Docker, mount the content/ directory as a volume so files persist across container restarts:

volumes:
- ./content:/app/content

  1. Go to console.cloud.google.comCloud Storage
  2. Create a bucket in your preferred region
  1. Go to IAM & Admin → Service AccountsCreate Service Account
  2. Give it a name (e.g. baan-gcs-access) — the service account ID is auto-filled

Create service account — name and ID

  1. Click Create and continue
  2. Under Grant this service account access to project, select the role Storage Object Admin

Grant Storage Object Admin role

  1. Click Done
  1. Open the service account you just created → Keys tab
  2. Click Add Key → Create new key

Add key — Create new key

  1. Select JSON (recommended) → Create

Create private key — select JSON

  1. The JSON key file downloads automatically. Store it securely — it cannot be re-downloaded if lost

Downloaded service account JSON

Baan reads the credentials from a single environment variable, so the JSON file must be encoded as one line.

Option A — Encode in your browser (no upload, no storage):

Encoded entirely in your browser. The file is never uploaded, never logged, and never stored.

Option B — Encode from your terminal:

Terminal window
base64 -i service-account.json | tr -d '\n'

On Linux, use base64 -w 0 service-account.json instead.

GOOGLE_CLOUD_PROJECT_ID=your-project-id
GCS_BUCKET_NAME=your-bucket-name
GOOGLE_CLOUD_CREDENTIALS_BASE64=base64-encoded-service-account-json

The GOOGLE_CLOUD_CREDENTIALS_BASE64 value is the entire contents of the JSON key file, Base64-encoded as a single line with no line breaks.


  1. Go to the S3 consoleCreate bucket
  2. Choose a region close to your users
  3. Keep “Block all public access” enabled (Baan accesses files directly, not via public URLs)

Create the policy first, then attach it to the IAM user in Step 2.

  1. Go to IAM → Policies → Create policy
  2. Switch to the JSON tab and paste the policy below — replace your-bucket-name with your actual bucket name
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BaanS3Access",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::your-bucket-name/*"
},
{
"Sid": "BaanS3List",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::your-bucket-name"
}
]
}

IAM Create policy — JSON editor with Baan policy

  1. Click Next → give the policy a name (e.g. baan-s3-access) → Create policy

Review and create — policy name

Step 2 — Create an IAM User and Attach the Policy

Section titled “Step 2 — Create an IAM User and Attach the Policy”
  1. Go to IAM → Users → Create user — give it a name (e.g. baan-s3-access)

Create user — Specify user details

  1. On the Set permissions screen, choose Attach policies directly, search for the policy you created in Step 1 (e.g. baan-s3-access), and select it

Attach policies directly — search and select

  1. Click Next → review the user and confirm the policy is attached → Create user

Review and create — user with attached policy

The user appears in the IAM users list once created.

IAM users list

  1. Open the user you just created → Security credentials tab → under Access keys, click Create access key

Access keys section — Create access key

  1. Select Application running outside AWS as the use case → Next

Access key best practices — Application running outside AWS

  1. Save the Access key ID and Secret access key — the secret is shown only once and cannot be retrieved later

Retrieve access keys — final screen

AWS_REGION=ap-northeast-1
AWS_S3_BUCKET_NAME=your-bucket-name
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key

R2 is S3-compatible and has no egress fees — your readers don’t pay bandwidth costs when assets are served from R2.

  1. Go to your Cloudflare dashboardR2
  2. Click Create bucket
  1. In R2 → Manage R2 API TokensCreate API Token
  2. Set permissions to Object Read & Write for your bucket
CF_ACCOUNT_ID=your-cloudflare-account-id
CF_R2_BUCKET_NAME=your-bucket-name
CF_R2_ACCESS_KEY_ID=your-api-token-key-id
CF_R2_SECRET_ACCESS_KEY=your-api-token-secret

Your CF_ACCOUNT_ID is shown in the right sidebar of any Cloudflare dashboard page.


Regardless of which provider you use, Baan organizes content in the same directory layout:

content/
├── article-slug/
│ ├── index.en.md # always index.{locale}.md — e.g. en, ja, fr
│ └── images/
│ └── hero.jpg
├── another-article/
│ ├── index.ja.md # multi-language: one file per locale
│ ├── index.en.md
│ └── images/
└── _settings/ # internal — do not modify manually
└── ...

System folders prefixed with _ (such as _settings/ and _auth/) are hidden in the Media Library and managed by Baan automatically.


Images referenced in articles are never exposed as direct signed storage URLs in the rendered HTML. Instead, Baan replaces them with stable proxy URLs:

/api/image?path=article-slug/images/photo.jpg

When a reader’s browser requests this URL, Baan generates a fresh signed URL from your storage provider and redirects the browser to it. The signed URL result is cached server-side for 24 hours — so repeated requests to the same image do not hit your storage API.

When you save or delete a post in the admin panel, Baan clears the image cache alongside the page cache, so updated images appear immediately.

Signed URL expiry (GCS / S3 / R2) is set to 48 hours — longer than the 24-hour server cache — to ensure the redirected URL is still valid for any visitor who lands on a cached response just before it is refreshed.