Skip to main content

Database setup

ExuluVariables requires a PostgreSQL table to store variables.

Table schema

CREATE TABLE variables (
  name VARCHAR(255) PRIMARY KEY,
  value TEXT NOT NULL,
  encrypted BOOLEAN DEFAULT false,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);
name
VARCHAR(255)
required
Unique variable identifier (primary key)
value
TEXT
required
The variable value (encrypted if encrypted is true)
encrypted
BOOLEAN
default:false
Whether the value is encrypted at rest (default: false)
created_at
TIMESTAMP
Timestamp when the variable was created
updated_at
TIMESTAMP
Timestamp when the variable was last updated

Creating the table

Run this migration in your PostgreSQL database:
-- Create variables table
CREATE TABLE IF NOT EXISTS variables (
  name VARCHAR(255) PRIMARY KEY,
  value TEXT NOT NULL,
  encrypted BOOLEAN DEFAULT false,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Add indexes for performance
CREATE INDEX idx_variables_encrypted ON variables(encrypted);
CREATE INDEX idx_variables_created_at ON variables(created_at);

-- Optional: Add audit columns
ALTER TABLE variables ADD COLUMN created_by VARCHAR(255);
ALTER TABLE variables ADD COLUMN updated_by VARCHAR(255);

Environment setup

Required environment variables

NEXTAUTH_SECRET
string
required
Secret key used for AES encryption/decryption of variable values
# .env file
NEXTAUTH_SECRET=your-long-random-secret-key-here
Critical: This secret must be:
  • At least 32 characters long
  • Randomly generated (use openssl rand -base64 32)
  • Kept secure and never committed to version control
  • Consistent across all deployments (production, staging, etc.)
Generate a secure secret:
# Using OpenSSL
openssl rand -base64 32

# Using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Database connection

ExuluVariables uses your PostgreSQL connection configured in postgresClient:
// Ensure your database connection is configured
import { postgresClient } from "@exulu/backend";

const { db } = await postgresClient();
Connection settings are typically configured via environment variables:
# .env file
DATABASE_URL=postgresql://user:password@localhost:5432/database
# or
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_DATABASE=database

Creating variables

Via SQL

-- Create an encrypted variable
INSERT INTO variables (name, value, encrypted)
VALUES ('openai_api_key', 'sk-...', true);

-- Create a non-encrypted variable
INSERT INTO variables (name, value, encrypted)
VALUES ('app_name', 'My App', false);

-- Bulk insert
INSERT INTO variables (name, value, encrypted) VALUES
  ('openai_api_key', 'sk-...', true),
  ('anthropic_api_key', 'sk-ant-...', true),
  ('google_api_key', 'AIza...', true),
  ('app_version', '1.0.0', false);
When inserting via SQL with encrypted: true, you must manually encrypt the value first using the same encryption method (AES with NEXTAUTH_SECRET).

Via API

Create a REST API endpoint to manage variables:
import { postgresClient, ExuluVariables } from "@exulu/backend";
import CryptoJS from "crypto-js";
import express from "express";

const app = express();
app.use(express.json());

// Create variable
app.post("/api/variables", async (req, res) => {
  const { name, value, encrypted = true } = req.body;

  const { db } = await postgresClient();

  // Encrypt if needed
  const finalValue = encrypted
    ? CryptoJS.AES.encrypt(value, process.env.NEXTAUTH_SECRET).toString()
    : value;

  await db.into("variables").insert({
    name,
    value: finalValue,
    encrypted
  });

  res.json({ success: true, name });
});

// Update variable
app.put("/api/variables/:name", async (req, res) => {
  const { name } = req.params;
  const { value, encrypted } = req.body;

  const { db } = await postgresClient();

  const finalValue = encrypted
    ? CryptoJS.AES.encrypt(value, process.env.NEXTAUTH_SECRET).toString()
    : value;

  await db("variables")
    .where({ name })
    .update({
      value: finalValue,
      encrypted,
      updated_at: new Date()
    });

  res.json({ success: true, name });
});

// Delete variable
app.delete("/api/variables/:name", async (req, res) => {
  const { name } = req.params;
  const { db } = await postgresClient();

  await db("variables").where({ name }).delete();

  res.json({ success: true, name });
});

// List variables (without values)
app.get("/api/variables", async (req, res) => {
  const { db } = await postgresClient();

  const variables = await db
    .from("variables")
    .select("name", "encrypted", "created_at", "updated_at");

  res.json(variables);
});
Never expose variable values through a public API. The above example shows creation/update but should be protected with authentication and authorization.

Via UI

Build a UI for managing variables:
// Frontend component (React example)
import { useState } from "react";

function VariableManager() {
  const [name, setName] = useState("");
  const [value, setValue] = useState("");
  const [encrypted, setEncrypted] = useState(true);

  const handleCreate = async () => {
    const response = await fetch("/api/variables", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name, value, encrypted })
    });

    if (response.ok) {
      alert("Variable created successfully!");
      setName("");
      setValue("");
    }
  };

  return (
    <div>
      <h2>Create Variable</h2>
      <input
        type="text"
        placeholder="Variable name"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <textarea
        placeholder="Variable value"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={encrypted}
          onChange={(e) => setEncrypted(e.target.checked)}
        />
        Encrypt value
      </label>
      <button onClick={handleCreate}>Create</button>
    </div>
  );
}

Variable configuration patterns

API keys (encrypted)

-- OpenAI
INSERT INTO variables (name, value, encrypted)
VALUES ('openai_api_key', 'sk-proj-...', true);

-- Anthropic
INSERT INTO variables (name, value, encrypted)
VALUES ('anthropic_api_key', 'sk-ant-api03-...', true);

-- Google
INSERT INTO variables (name, value, encrypted)
VALUES ('google_api_key', 'AIza...', true);

-- Cohere
INSERT INTO variables (name, value, encrypted)
VALUES ('cohere_api_key', 'abc123...', true);

Database credentials (encrypted)

-- Full connection string
INSERT INTO variables (name, value, encrypted)
VALUES ('postgres_url', 'postgresql://user:pass@host:5432/db', true);

-- Redis connection
INSERT INTO variables (name, value, encrypted)
VALUES ('redis_url', 'redis://:password@localhost:6379', true);

Service tokens (encrypted)

-- GitHub
INSERT INTO variables (name, value, encrypted)
VALUES ('github_token', 'ghp_...', true);

-- Stripe
INSERT INTO variables (name, value, encrypted)
VALUES ('stripe_secret_key', 'sk_live_...', true);

-- SendGrid
INSERT INTO variables (name, value, encrypted)
VALUES ('sendgrid_api_key', 'SG....', true);

Configuration values (non-encrypted)

-- App metadata
INSERT INTO variables (name, value, encrypted)
VALUES ('app_name', 'My Application', false);

INSERT INTO variables (name, value, encrypted)
VALUES ('app_version', '1.0.0', false);

-- Feature flags
INSERT INTO variables (name, value, encrypted)
VALUES ('feature_analytics', 'true', false);

-- Limits and quotas
INSERT INTO variables (name, value, encrypted)
VALUES ('max_requests_per_minute', '100', false);

Multi-environment variables

-- Development
INSERT INTO variables (name, value, encrypted)
VALUES ('dev_openai_api_key', 'sk-...', true);

-- Staging
INSERT INTO variables (name, value, encrypted)
VALUES ('staging_openai_api_key', 'sk-...', true);

-- Production
INSERT INTO variables (name, value, encrypted)
VALUES ('prod_openai_api_key', 'sk-...', true);
Usage:
const env = process.env.NODE_ENV || "dev";
const apiKey = await ExuluVariables.get(`${env}_openai_api_key`);

Multi-tenant variables

-- Per-tenant API keys
INSERT INTO variables (name, value, encrypted) VALUES
  ('tenant_acme_openai_key', 'sk-...', true),
  ('tenant_globex_openai_key', 'sk-...', true),
  ('tenant_initech_openai_key', 'sk-...', true);
Usage:
async function getTenantApiKey(tenantId: string) {
  return await ExuluVariables.get(`tenant_${tenantId}_openai_key`);
}

Encryption management

Manual encryption (when inserting via SQL)

If you need to insert encrypted variables directly via SQL:
import CryptoJS from "crypto-js";

// Encrypt a value
const plaintext = "sk-proj-abc123...";
const encrypted = CryptoJS.AES.encrypt(
  plaintext,
  process.env.NEXTAUTH_SECRET
).toString();

console.log(encrypted);
// Use this encrypted value in your INSERT statement
INSERT INTO variables (name, value, encrypted)
VALUES ('openai_api_key', 'U2FsdGVkX1...', true);

Manual decryption (for testing)

import CryptoJS from "crypto-js";

const encrypted = "U2FsdGVkX1...";
const bytes = CryptoJS.AES.decrypt(encrypted, process.env.NEXTAUTH_SECRET);
const decrypted = bytes.toString(CryptoJS.enc.Utf8);

console.log(decrypted); // "sk-proj-abc123..."

Rotating the encryption key

If you need to change your NEXTAUTH_SECRET:
1

Retrieve all encrypted variables

import { postgresClient, ExuluVariables } from "@exulu/backend";

const { db } = await postgresClient();
const encrypted = await db.from("variables").where({ encrypted: true });

// Decrypt all with old key
const decrypted = await Promise.all(
  encrypted.map(async (v) => ({
    name: v.name,
    value: await ExuluVariables.get(v.name) // Uses old key
  }))
);
2

Update NEXTAUTH_SECRET

# Update your .env file
NEXTAUTH_SECRET=new-secret-key-here
3

Re-encrypt variables

import CryptoJS from "crypto-js";

for (const { name, value } of decrypted) {
  const encrypted = CryptoJS.AES.encrypt(
    value,
    process.env.NEXTAUTH_SECRET // New key
  ).toString();

  await db("variables")
    .where({ name })
    .update({ value: encrypted });
}
4

Verify

// Test retrieval with new key
const testValue = await ExuluVariables.get("openai_api_key");
console.log("Success:", testValue);
Rotating encryption keys is risky. Test thoroughly and have backups before proceeding.

Migration from .env files

Step 1: Identify secrets

List all secrets currently in .env:
# .env
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
DATABASE_URL=postgresql://...
REDIS_URL=redis://...

Step 2: Create variables

import { postgresClient } from "@exulu/backend";
import CryptoJS from "crypto-js";
import dotenv from "dotenv";

dotenv.config();

async function migrateFromEnv() {
  const { db } = await postgresClient();

  const secrets = [
    { name: "openai_api_key", envKey: "OPENAI_API_KEY" },
    { name: "anthropic_api_key", envKey: "ANTHROPIC_API_KEY" },
    { name: "database_url", envKey: "DATABASE_URL" },
    { name: "redis_url", envKey: "REDIS_URL" }
  ];

  for (const { name, envKey } of secrets) {
    const value = process.env[envKey];
    if (!value) continue;

    const encrypted = CryptoJS.AES.encrypt(
      value,
      process.env.NEXTAUTH_SECRET
    ).toString();

    await db.into("variables").insert({
      name,
      value: encrypted,
      encrypted: true
    });

    console.log(`Migrated ${name}`);
  }
}

migrateFromEnv();

Step 3: Update code

Replace process.env with ExuluVariables.get():
// Before
const apiKey = process.env.OPENAI_API_KEY;

// After
import { ExuluVariables } from "@exulu/backend";
const apiKey = await ExuluVariables.get("openai_api_key");

Step 4: Remove from .env

After verifying everything works, remove secrets from .env:
# .env (keep only NEXTAUTH_SECRET and non-secret config)
NEXTAUTH_SECRET=your-secret
NODE_ENV=production
PORT=3000

Best practices

Set encrypted: true for API keys, tokens, passwords, connection strings, and any other secrets.
-- ✅ Good
INSERT INTO variables (name, value, encrypted)
VALUES ('api_key', 'sk-...', true);

-- ❌ Bad
INSERT INTO variables (name, value, encrypted)
VALUES ('api_key', 'sk-...', false);
Variable names should be clear and avoid conflicts.
-- ✅ Good
'openai_api_key'
'tenant_123_stripe_key'
'prod_database_url'

-- ❌ Bad
'key1'
'api'
'secret'
Maintain a list of required variables for your application.
// config/variables.ts
export const REQUIRED_VARIABLES = [
  "openai_api_key",
  "anthropic_api_key",
  "database_url",
  "redis_url"
];

// Validate on startup
async function validateVariables() {
  for (const name of REQUIRED_VARIABLES) {
    try {
      await ExuluVariables.get(name);
    } catch (error) {
      throw new Error(`Missing required variable: ${name}`);
    }
  }
}
Regularly backup the variables table and keep your NEXTAUTH_SECRET secure.
# Backup variables table
pg_dump -U user -d database -t variables > variables_backup.sql

# Store NEXTAUTH_SECRET separately in secure location
echo "NEXTAUTH_SECRET=..." > .secret.backup
Restrict database access to the variables table.
-- Create read-only user for application
CREATE USER app_user WITH PASSWORD 'secure_password';
GRANT SELECT ON variables TO app_user;

-- Admin user can create/update/delete
GRANT ALL PRIVILEGES ON variables TO admin_user;
Always wrap ExuluVariables.get() in try/catch.
try {
  const apiKey = await ExuluVariables.get("openai_api_key");
  // Use apiKey
} catch (error) {
  console.error("Failed to retrieve API key:", error);
  // Handle error (use fallback, alert admin, etc.)
}

Troubleshooting

Variable not found

// Error: Variable my_api_key not found.
Solution: Ensure the variable exists in the database:
SELECT * FROM variables WHERE name = 'my_api_key';
If missing, create it:
INSERT INTO variables (name, value, encrypted)
VALUES ('my_api_key', 'value', true);

Decryption fails

// Error: Malformed UTF-8 data
Causes:
  • Wrong NEXTAUTH_SECRET (different from when variable was encrypted)
  • Variable was not actually encrypted but encrypted: true is set
  • Corrupted encrypted value
Solution: Re-encrypt the variable with the correct secret.

Database connection error

// Error: Connection refused
Solution: Verify your database connection settings:
# Check environment variables
echo $DATABASE_URL

# Test connection
psql $DATABASE_URL -c "SELECT 1"

Encryption key too short

If using a short NEXTAUTH_SECRET:
// Weak encryption with short key
Solution: Use a properly generated secret:
openssl rand -base64 32

Next steps