fora
← Docs

Postgres connector

i18n as infrastructure

Point the Fora connector at your Postgres database with a config file. It creates a fora_translations table and optional locale-transparent views. Your app queries the views with a session variable — no other code changes required.

How it works

Your DB ←──── connector (your VPC) ────→ Fora API
               ↓
     fora_translations
               ↓
       products_localized  (view)
               ↑
          Your app queries here

The connector runs entirely in your infrastructure. Your database credentials never leave your network. The only outbound connection is HTTPS to api.getfora.ai. Translated images are written directly from the Fora server to your S3 bucket via pre-signed PUT URLs.

1. Install the connector

Pull the connector image from GitHub Container Registry:

docker pull ghcr.io/getfora/fora-connector:latest

Works on Linux and macOS (Intel and Apple Silicon). No other dependencies required.

2. Configure it

Create fora.yaml next to your deployment config:

name: my-app

# Postgres DSN — connector needs SELECT on your tables,
# CREATE TABLE / CREATE VIEW on the schema.
connection_string: postgres://user:password@localhost:5432/myapp?sslmode=disable

config:
  target_locales:
    - de
    - es
    - fr
    - it
    - pt-BR

  # Terms preserved untranslated across all tables (brand names, product codes, etc.)
  protected_terms:
    - Nike
    - AirForce 1

  # Creates <table>_localized views. Your app queries these instead of base tables.
  create_views: true

  tables:
    - table: products
      id_column: id
      fields:
        - name
        - description
      filter: "status = 'active'"   # optional WHERE clause

    - table: posts
      id_column: id
      fields:
        - title
        - body
      image_fields:
        - hero_image_url             # requires s3 config below

# Required only if translating images
s3:
  bucket: my-app-translations
  region: us-east-1
  # Credentials are optional — omit them if the connector runs on EC2, ECS, or EKS
  # with an IAM role that has s3:PutObject on this bucket.
  # access_key_id: AKIA...
  # secret_access_key: ...

3. Run the connector

docker run --rm \
  -v $(pwd)/fora.yaml:/fora.yaml \
  -e FORA_API_KEY=fora_live_abc123... \
  ghcr.io/getfora/fora-connector:latest /fora.yaml

On first run the connector:

  1. Creates fora_translations in your database
  2. Creates products_localized and posts_localized views
  3. Translates all matching rows and writes results to the translations table
  4. Enters a poll loop (every 30 seconds) to pick up new rows automatically
level=INFO msg="provisioning schema" tables=2 locales=[de es fr it pt-BR]
level=INFO msg="connector started" version=v0.2.0 poll_interval=30s
level=INFO msg="translated row" table=products row_id=1 locale=es
level=INFO msg="translated row" table=products row_id=1 locale=fr
...

Re-running is safe — only rows without a completed translation are processed.

4. Query localized content

The _localized views return translated content when fora.locale is set, and fall back to the original column value otherwise — so your app never returns a blank field for rows the connector hasn't processed yet. The variable must be set inside a transaction:

BEGIN;
SET LOCAL fora.locale = 'es';
SELECT id, name, description FROM products_localized WHERE status = 'active';
COMMIT;

Go (pgx)

func withLocale(ctx context.Context, pool *pgxpool.Pool, locale string, fn func(pgx.Tx) error) error {
    tx, err := pool.Begin(ctx)
    if err != nil { return err }
    defer tx.Rollback(ctx)
    if _, err := tx.Exec(ctx, "SELECT set_config('fora.locale', $1, true)", locale); err != nil { return err }
    if err := fn(tx); err != nil { return err }
    return tx.Commit(ctx)
}

Node.js (node-postgres)

async function withLocale(pool, locale, fn) {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    await client.query("SELECT set_config('fora.locale', $1, true)", [locale]);
    const result = await fn(client);
    await client.query('COMMIT');
    return result;
  } catch (err) {
    await client.query('ROLLBACK');
    throw err;
  } finally {
    client.release();
  }
}

Python (psycopg3)

from contextlib import contextmanager

@contextmanager
def locale_transaction(conn, locale):
    with conn.transaction():
        conn.execute("SELECT set_config('fora.locale', %s, true)", [locale])
        yield conn

# Usage:
with locale_transaction(conn, request.locale) as c:
    rows = c.execute("SELECT id, name FROM products_localized").fetchall()

Ruby on Rails

def with_locale(locale, &block)
  ApplicationRecord.transaction do
    ApplicationRecord.connection.execute(
      ApplicationRecord.sanitize_sql(["SET LOCAL fora.locale = ?", locale])
    )
    block.call
  end
end

with_locale(I18n.locale) { Product.from("products_localized").active.all }

Production deployment

For long-running deployments, add the connector as a service alongside your app:

services:
  fora-connector:
    image: ghcr.io/getfora/fora-connector:latest
    environment:
      FORA_API_KEY: ${FORA_API_KEY}
      DATABASE_URL: postgres://user:password@db:5432/myapp
    volumes:
      - ./fora.yaml:/fora.yaml:ro
    command: ["/fora.yaml"]
    restart: unless-stopped

On ECS or Kubernetes, run it as a sidecar or standalone service. The connector only needs outbound HTTPS to api.getfora.ai and access to your Postgres host.

Database permissions

-- Minimum required
GRANT SELECT ON products, posts TO fora_connector;
GRANT CREATE ON SCHEMA public TO fora_connector;

If you prefer not to grant CREATE, create the table manually first:

CREATE TABLE fora_translations (
    table_name TEXT        NOT NULL,
    row_id     TEXT        NOT NULL,
    locale     TEXT        NOT NULL,
    fields     JSONB       NOT NULL DEFAULT '{}',
    status     TEXT        NOT NULL DEFAULT 'pending',
    synced_at  TIMESTAMPTZ,
    PRIMARY KEY (table_name, row_id, locale)
);
CREATE INDEX fora_translations_pending
    ON fora_translations (table_name, status)
    WHERE status = 'pending';
GRANT INSERT, UPDATE, SELECT ON fora_translations TO fora_connector;

Configuration reference

FieldReqDescription
nameNoIdentifier shown in logs
connection_stringYesPostgres DSN. Overridden by DATABASE_URL env var
config.target_localesYesBCP-47 codes: de, es, fr, it, pt-BR
config.protected_termsNoTerms to preserve untranslated across all tables — brand names, product codes, etc. Max 50, 100 chars each.
config.create_viewsNoCreate <table>_localized views (default: false)
config.tables[].tableYesTable name
config.tables[].id_columnYesPrimary key column
config.tables[].fieldsYesText columns to translate
config.tables[].image_fieldsNoImage URL columns (requires s3 config)
config.tables[].filterNoSQL WHERE clause to scope rows
s3.bucketNoS3 bucket name (required for image translation)
s3.regionNoAWS region (default: us-east-1)
s3.access_key_idNoAWS access key. Omit when running on EC2/ECS/EKS with an IAM role — the standard credential chain is used automatically.
s3.secret_access_keyNoAWS secret key. Never commit to source control — use an IAM role or pass via environment variable instead.
s3.key_prefixNoKey prefix for translated image objects
s3.endpointNoCustom endpoint for R2 / MinIO
s3.public_urlNoCDN base URL (required for custom endpoints)

Environment variables

FieldReqDescription
FORA_API_KEYYesAPI key
DATABASE_URLNoOverrides connection_string in config file
FORA_API_URLNoOverride API base URL (default: https://api.getfora.ai)
POLL_INTERVALNoSeconds between sync runs (default: 30)