Quick Start

Scaffold and run your first EFC API in under a minute. You need Node.js 18 or later; MongoDB, PostgreSQL or Redis only if you choose features that use them.

Create a project

$ npx create-efc-app my-api

The interactive wizard asks:

? Project name:            my-api
? Language:                TypeScript | JavaScript
? Database:                MongoDB | PostgreSQL
? Models:                  defineTable | defineModel        (PostgreSQL only)
? Authentication strategy: http-only | localStorage
? Features: (space to toggle)
    ◉ Multi-core clustering
    ◉ Background tasks
    ◉ API route documentation
    ◉ User portal
    ◉ Admin portal
    ◉ Role-based access control
    ◯ Mailer

Some choices ask a follow-up:

  • Background tasks → a queue backend: BullMQ (Redis) or pg-boss (stores jobs in your PostgreSQL — no Redis to run).
  • Mailer → Gmail (preconfigured, App Password prompt) or a custom SMTP host. See Mailer.
  • User / Admin portal → which sub-features to generate. See Generated Portals.
  • RBAC → a Role model and requireAuth('role') on protected routes. See RBAC.

It then writes the project, generates .env with a real JWT_SECRET, and runs npm install.

Start the dev server

$ cd my-api
$ npm run dev

npm run dev runs efc start dev: one process with hot reload. Tables and indexes are created the first time a model is used, so there's no migration to run first. The scaffolded src/api/health.ts answers at http://localhost:3000/v1/api/health — /v1/api is the default basePath.

In development, http://localhost:3000/ shows the API dashboard: every route, with its methods and documentation.

Add a route

$ efc generate route posts/[slug]
# → Created src/api/posts/[slug].ts

That's it — the file is registered as /posts/:slug the next time the server reloads. Check the result any time with efc routes.

Next

Project Structure

Every EFC project follows the same layout. src/api/ and src/tasks/ are fixed conventions — there is no option to move them.

my-api/
├── src/
│   ├── api/                       # HTTP routes — every file is a route
│   │   ├── health.ts              # GET /health
│   │   ├── auth/
│   │   │   ├── login.ts           # POST /auth/login
│   │   │   └── logout.ts          # POST /auth/logout
│   │   └── users/
│   │       ├── index.ts           # GET /users · POST /users
│   │       └── [id].ts            # GET /users/:id · DELETE /users/:id
│   ├── tasks/                     # background jobs (scanned non-recursively)
│   │   └── SendEmail.ts           # task name: "SendEmail"
│   ├── model/                     # models
│   │   ├── User.ts
│   │   └── Role.ts                # with the RBAC option
│   ├── migrations/                # efc db migrate — run in filename order
│   ├── seeds/                     # efc db seed
│   └── index.ts                   # entry point — calls ignite()
├── efc.config.ts                  # builds the config ignite() receives
├── .env                           # secrets — gitignored
├── .env.example                   # documented template — committed
├── package.json
└── tsconfig.json

Conventions

  • Every .ts / .js file in src/api/ becomes a route; its path is the URL.
  • [brackets] in a file or folder name become URL parameters.
  • index.ts maps to its folder's path.
  • Every file directly inside src/tasks/ registers a task named after the file. Subfolders are not scanned.
  • src/index.ts is what runs. efc.config.ts only takes effect because src/index.ts imports it.

Production builds

efc build prod transpiles every file under src/ into dist/src/ without bundling — routes and tasks are discovered by scanning the filesystem, so they must exist as files. efc start prod runs dist/src/index.js.

Enabling User portal or Admin portal fills src/model/ and src/api/ out considerably — see Generated Portals.

Configuration

ignite() takes one config object. It never reads efc.config.ts or .env by itself — you import the config and pass it in, so what you see is what runs.

efc.config.ts

The scaffolder generates a config that reads each environment variable explicitly:

import type { EFCConfig } from 'express-file-cluster'

const config: EFCConfig = {
  port: process.env.PORT ? Number(process.env.PORT) : undefined,
  databaseUrl: process.env.DATABASE_URL,
  jwtSecret: process.env.JWT_SECRET,
  authStrategy: 'http-only',
  tasks: { backend: 'bullmq', redisUrl: process.env.REDIS_URL },
  cache: { backend: 'redis', url: process.env.REDIS_URL },
  globalMiddlewares: [],
}

export default config

src/index.ts

import { ignite, gracefulShutdown } from 'express-file-cluster'
import config from '../efc.config.js'

ignite({ ...config, cluster: true })
  .then(gracefulShutdown)
  .catch(console.error)

Options

OptionDefaultDescription
basePath'/v1/api'Prefix for every route. '/' mounts at the root.
port3000HTTP listen port.
clusteron in productionFork one worker per CPU. See Clustering.
workersCPU countNumber of workers to fork.
databasefrom URL'mongodb' or 'postgresql'; detected from databaseUrl when omitted.
databaseUrl—Connection string.
authStrategy'http-only''http-only' cookie or 'localStorage' Bearer token.
jwtSecret—Enables auth. At least 32 random bytes in production.
jwtExpiresIn'7d'Token lifetime: '15m', '1h', '30d'…
tasksoffQueue backend. See Background Tasks.
cacheoff{ backend: 'redis' | 'memory' } — also the session-revocation store.
loggingonpino options; logging.queries turns on query logging.
requestTimeout—Milliseconds before a request is answered 408.
cors—true or a CORS config.
globalMiddlewares[]Run before every route.
onErrorbuilt inReplace the global error handler.
autoSynctrueReconcile tables / collections on a model's first use.
dashboardon in devfalse disables the development API dashboard.

File-Based Routing

EFC scans src/api/ at startup and builds the route map from the file tree. No registration — add a file, get a route.

Routing rules

Take the path relative to src/api, drop the extension and a trailing /index, then convert each segment:

FileURLreq.params
health.ts/health—
users/index.ts/users—
users/[id].ts/users/:idid: string
posts/[slug]/comments.ts/posts/:slug/commentsslug: string
users/[[id]].ts/users and /users/:idid?: string
docs/[...slug].ts/docs/a/b (one or more)slug: ['a', 'b']
docs/[[...slug]].ts/docs, /docs/a/b (zero or more)slug: string[]

Every route is mounted under basePath — /v1/api unless you change it. A catch-all must be the last segment; anything else is a startup error.

Specificity

Routes register most specific first, comparing one segment at a time: static, then [id], then [[id]], then [...slug], then [[...slug]]. users/me.ts therefore always matches before users/[id].ts, regardless of file order.

Method dispatching

Export uppercase HTTP method names — GET POST PUT PATCH DELETE HEAD OPTIONS. A method the file doesn't export answers 405 Method Not Allowed with an Allow header.

export const GET    = async (req, res) => { /* ... */ }
export const PUT    = async (req, res) => { /* ... */ }
export const PATCH  = async (req, res) => { /* ... */ }
export const DELETE = async (req, res) => { /* ... */ }

Route metadata

Export a meta object to document the route in the API dashboard. It is keyed by HTTP method, so every method gets its own description and examples. All fields are optional.

export const meta = {
  GET: {
    description: 'Fetch a user by ID.',
    request: { params: { id: 'usr_01HXZ' } },
    response: {
      status: 200,
      body: { id: 'usr_01HXZ', name: 'Ada Lovelace', createdAt: '2026-01-01T00:00:00.000Z' },
    },
  },
  DELETE: {
    description: 'Delete a user by ID.',
    response: { status: 204 },
  },
}

Route Handlers

Handlers are plain Express async (req, res) => {} functions. Thrown errors and rejected promises are caught for you.

Static route

// GET /health
export const GET = async (req, res) => {
  res.json({ status: 'OK', ts: new Date().toISOString() })
}

Dynamic route

// GET /users/:id
import { HttpError } from 'express-file-cluster'
import { User } from '../../model/User.js'

export const GET = async (req, res) => {
  const user = await User.findById(req.params.id)
  if (!user) throw new HttpError(404, 'User not found')
  res.json(user)
}

Validating input

export const schema validates params, query and body with any Standard Schema validator — zod, valibot, arktype. It runs after the file's middlewares. Failures answer 400 with issues; on success the parsed output replaces the input, so coercions and defaults apply.

import { z } from 'zod'

// one schema for every method in the file…
export const schema = {
  params: z.object({ id: z.string().uuid() }),
}

// …or keyed by method
export const schema = {
  GET:   { query: z.object({ include: z.enum(['reviews', 'stock']).optional() }) },
  PATCH: { body: z.object({ price: z.number().positive() }) },
}
{
  "error": "Validation failed",
  "statusCode": 400,
  "issues": [{ "path": ["params", "id"], "message": "Invalid uuid" }]
}

Middleware

Three tiers, each with a clear scope. Pick the narrowest one that does the job.

1. Global middleware

Set in the config — runs before every route.

import rateLimit from 'express-rate-limit'

const config: EFCConfig = {
  globalMiddlewares: [rateLimit({ windowMs: 60_000, max: 100 })],
}

2. Route-level middleware

Export a middlewares array; it applies to every method in the file.

import { requireAuth } from 'express-file-cluster/auth'

export const middlewares = [requireAuth('admin')]

export const GET = async (req, res) => { /* ... */ }
export const DELETE = async (req, res) => { /* ... */ }

3. Handler-level, with compose()

For a guard that applies to one method only.

import { compose } from 'express-file-cluster'

export const POST = compose(
  checkQuota,
  async (req, res) => {
    res.status(201).json(await User.create(req.body))
  },
)

Order

For one request: global middlewares → the file's middlewares → schema validation → the handler (including anything it composes).

Error Handling

Throw from any handler or middleware — EFC catches it and sends a clean JSON response.

HttpError

import { HttpError } from 'express-file-cluster'

export const GET = async (req, res) => {
  const user = await User.findById(req.params.id)
  if (!user) throw new HttpError(404, 'User not found')
  res.json(user)
}
{ "error": "User not found", "statusCode": 404 }

Any other thrown error becomes a 500 with "Internal Server Error" — the message is not leaked to the client.

Custom global handler

import { HttpError } from 'express-file-cluster'
import type { ErrorRequestHandler } from 'express'

const onError: ErrorRequestHandler = (err, req, res, _next) => {
  const status = err instanceof HttpError ? err.statusCode : 500
  res.status(status).json({
    error: status === 500 ? 'Internal Server Error' : err.message,
    requestId: req.headers['x-request-id'],
  })
}

const config: EFCConfig = { onError }

Response reference

ScenarioStatus
throw new HttpError(n, msg)n
Any other thrown error500
Method not exported by the route file405 + Allow
Route schema validation failed400 + issues
Model validation failed (ModelValidationError)400 + issues
Unique constraint violated409
requestTimeout exceeded408
Missing or invalid token401
Role not allowed by requireAuth('role')403

Clustering

With cluster: true — the default when NODE_ENV=production — the primary forks one worker per CPU. All workers share the port and the operating system spreads connections across them.

Pre-Flight, per worker

Every worker boots independently, in this order:

  1. Configure logging (before the primary/worker split)
  2. Create Express: request context and IDs, requestTimeout, CORS, JSON, cookies
  3. Connect the database and set the model engine, then the cache
  4. Configure auth (when jwtSecret is set)
  5. Scan and register tasks from src/tasks
  6. Start the queue backend — cron schedules sync in the background
  7. Scan routes from src/api
  8. Mount routes
  9. Listen

Self-healing

When a worker exits unexpectedly, the primary calls onWorkerCrash and forks a replacement, which runs the full Pre-Flight before taking traffic.

ignite({
  ...config,
  cluster: true,
  workers: 4, // default: CPU count
  onWorkerReady: (id) => logger.info(`Worker ${id} ready`),
  onWorkerCrash: (id, code) => alerting.send(`Worker ${id} crashed (${code})`),
})

Graceful shutdown

gracefulShutdown(server) handles SIGTERM and SIGINT. A worker stops accepting connections, lets in-flight requests and task jobs finish, then exits (forced after 10 s). The primary stops respawning and relays the signal to every worker — so docker stop and Kubernetes, which signal only PID 1, don't hang.

ignite(config).then((server) => gracefulShutdown(server, 30_000)) // 30 s drain window

In development, efc start dev runs a single process with hot reload.

Background Tasks

Define tasks in src/tasks/, enqueue them from any route and respond immediately — the job runs off the request path.

Configure a backend

tasks: { backend: 'bullmq', redisUrl: process.env.REDIS_URL } // Redis
tasks: { backend: 'pg-boss' }                                // your PostgreSQL (databaseUrl)

Define a task

The task name is the file name, case-sensitive. A default export is required.

import { defineTask } from 'express-file-cluster/tasks'

export default defineTask<{ to: string; subject: string }>(async (payload) => {
  await mailer.send(payload)
})

CPU-bound work

thread: true runs the handler in worker_threads, keeping the event loop free.

export default defineTask<{ key: string; width: number }>(
  { thread: true, retries: 5 },
  async ({ key, width }) => {
    const out = await sharp(await download(key)).resize(width).toBuffer()
    await upload(`${key}@${width}`, out)
  },
)

Enqueue

import { enqueue } from 'express-file-cluster/tasks'

export const POST = async (req, res) => {
  const user = await User.create(req.body)
  await enqueue('SendEmail', { to: user.email, subject: 'Welcome' })
  res.status(202).json({ id: user.id, queued: true })
}

enqueue rejects if no backend is configured, the task name doesn't exist, or the backend doesn't accept the job within tasks.enqueueTimeout (default 5000 ms) — it never hangs, so it's safe to answer 503.

Task options

OptionDefaultDescription
threadfalseRun in worker_threads.
retries3Re-runs after a failure — up to retries + 1 attempts before dead-lettering.
backoff'exponential''fixed' or 'exponential' (1 s, doubling).
concurrencytasks.concurrencyParallel jobs of this task.
schedule—Cron pattern; scheduled runs receive {}. Schedules reconcile on boot.
timezoneserver timeIANA timezone for schedule.

Dead letters

$ efc tasks failed --name SendEmail   # error, attempts, payload
$ efc tasks retry --all                 # re-queue
$ efc tasks purge --all                 # delete failed jobs

Authentication

JWT auth, configured by jwtSecret and authStrategy. The scaffolder asks for the strategy and puts a generated secret in .env.

http-only cookie (recommended for browsers)

The token travels in an HttpOnly + Secure + SameSite=Strict cookie named efc_token.

import { issueToken } from 'express-file-cluster/auth'

export const POST = async (req, res) => {
  const user = await verifyCredentials(req.body)
  await issueToken(res, { sub: user.id, role: user.role })
  res.json({ message: 'Logged in' })
}

Bearer token (SPAs, mobile)

With authStrategy: 'localStorage', return the token and send it back as Authorization: Bearer <token>.

import { signToken } from 'express-file-cluster/auth'

export const POST = async (req, res) => {
  const user = await verifyCredentials(req.body)
  res.json({ token: await signToken({ sub: user.id, role: user.role }) })
}

Protecting routes

import { requireAuth, getCurrentUser } from 'express-file-cluster/auth'

export const middlewares = [requireAuth] // 401 without a valid token

export const GET = async (req, res) => {
  res.json({ me: req.user, alsoMe: getCurrentUser() })
}

getCurrentUser() returns the token payload anywhere in the request — models and helpers included.

Logging out

CallEffect
revokeToken(res)Clears the cookie only. The token itself stays valid until it expires.
revokeSession(req, res?)Rejects the current token from now on (and clears the cookie). Needs cache.
revokeAllSessions(userId)Rejects every token issued to the user so far — "log out everywhere". Needs cache.

Role-Based Access Control

Role checks are built into requireAuth — call it with role names instead of reaching for a separate middleware.

requireAuth(...roles)

import { requireAuth } from 'express-file-cluster/auth'

// bare — authentication only
export const middlewares = [requireAuth]

// authentication + role — 403 unless payload.role is one of these
export const middlewares = [requireAuth('admin')]
export const middlewares = [requireAuth('user', 'admin')]

Generated by the scaffolder

Toggle Role-based access control in create-efc-app and every protected route it generates uses this shorthand instead of an inline user.role === 'admin' check. It also generates a Role model and — with the Admin portal — a full /admin/roles CRUD API.

Database

Two model APIs. defineModel runs unchanged on MongoDB (mongoose) and PostgreSQL (Drizzle). defineTable is PostgreSQL-native, with Prisma's query vocabulary and real relations.

The engine follows database, or the databaseUrl scheme. Install mongoose for MongoDB, or pg and drizzle-orm for PostgreSQL.

defineModel

import { defineModel, type InferRecord } from 'express-file-cluster'

export const User = defineModel('User', {
  name:  { type: 'string', required: true },
  email: { type: 'string', required: true, unique: true },
  role:  { type: 'string', enum: ['member', 'admin'], default: 'member' },
})

export type UserRecord = InferRecord<typeof User> // no interface to write

Methods

MethodReturns
find(filter?, { sort, limit, skip, select, populate })records
findOne(filter) · findById(id)record | null
count(filter?) · exists(filter)number · boolean
distinct(field, filter?)sorted, non-null values
create(data) · createMany(list)record(s)
update(id, data) · updateOne(filter, data)record | null
updateMany(filter, data)number modified
upsert(filter, create, update)record
delete(id) · deleteOne(filter) · deleteMany(filter)void · record | null · number

Filters $eq $ne $gt $gte $lt $lte $in $nin $exists $regex $and $or, dotted paths and array-contains behave the same on both engines; updates support $set $unset $inc $push $pull. Invalid fields throw ModelValidationError (400); a duplicate unique value is a 409.

defineTable (PostgreSQL)

import { defineTable } from 'express-file-cluster'

export const Post = defineTable('Post', {
  title:  { type: 'string', required: true },
  author: { type: 'uuid', ref: 'User', onDelete: 'cascade' }, // foreign key
  tags:   { type: 'array', of: 'uuid', ref: 'Tag' },          // many-to-many
})

const posts = await Post.findMany({
  where:   { tags: { some: { label: 'efc' } } },
  include: { author: true, tags: true }, // one SQL statement, any depth
  orderBy: { createdAt: 'desc' },
  take:    20,
})

Also findUnique, upsert (INSERT … ON CONFLICT), groupBy, aggregate, relation filters and nested writes. Full reference: defineTable ↗.

Schema sync

A model's table or collection — columns, indexes, foreign keys — is reconciled on first use (autoSync, default on). Sync adds and converts; it never drops a column or a row. Preview changes with efc db sync --dry-run, or set autoSync: false and use migrations.

Transactions

import { transaction } from 'express-file-cluster'

await transaction(async () => {
  const order = await Order.create({ customer, items })
  await Stock.updateMany({ id: { $in: ids } }, { $inc: { count: -1 } })
}) // a throw rolls everything back

Raw client

import { db } from 'express-file-cluster'
// MongoDB: a mongoose Connection · PostgreSQL: a pg Pool
const { rows } = await db.query('SELECT * FROM users WHERE id = $1', [id])

db is only available after Pre-Flight — don't touch it at module load time.

Mailer

Toggle Mailer in the scaffolder for a working nodemailer transport, wired to environment variables, and a real SendEmail task.

Choose a provider

? Email provider:
  ❯ Gmail                 smtp.gmail.com, preconfigured
    Other (custom SMTP)   you'll provide host + port

With Gmail no host or port is asked — SMTP_HOST / SMTP_PORT are set to smtp.gmail.com / 465. Other prompts for both.

Gmail needs an App Password

Google rejects your normal password over SMTP. The wizard accepts only a 16-character App Password:

The generated task

import { defineTask } from 'express-file-cluster/tasks'
import nodemailer from 'nodemailer'

const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: Number(process.env.SMTP_PORT ?? 587),
  secure: Number(process.env.SMTP_PORT) === 465,
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
})

export default defineTask(async (payload) => {
  await transporter.sendMail({
    from: process.env.SMTP_FROM ?? process.env.SMTP_USER,
    to: payload.to,
    subject: payload.subject,
    html: payload.body,
  })
})

Send mail with enqueue('SendEmail', payload) — see Background Tasks. With the User portal enabled too, registration, password reset and email verification enqueue it for you.

API Dashboard

In development (NODE_ENV=development), GET / serves a live page listing every route — one card per HTTP method, with its description and request/response examples. No external tooling.

Turning it on and off

It is on by default in development and does not exist in production. Set dashboard: false to disable it in development too.

Documenting a route

The cards come from each file's meta export, keyed by method. A method without an entry still gets a card, marked undocumented.

export const meta = {
  GET: {
    description: 'Fetch a user by ID.',
    request: {
      headers: { Authorization: 'Bearer <token>' },
      params: { id: 'usr_01HXZ' },
    },
    response: {
      status: 200,
      body: { id: 'usr_01HXZ', name: 'Ada Lovelace', createdAt: '2026-01-01T00:00:00.000Z' },
    },
  },
}

Type rendering

Response bodies are shown as type names, not your sample values — examples stay readable without leaking data.

Value in metaShown as
'Ada Lovelace'String
42891Number
trueBoolean
'2026-01-01T00:00:00.000Z'Date (ISO strings are detected)

Generated Portals

The User portal and Admin portal options scaffold a large, working set of models and routes — not stubs. You pick the sub-features you want, and only what they need is generated.

Every model is real on both databases: defineModel collections on MongoDB; on PostgreSQL, defineTable models (or defineModel, if you chose it) linked by foreign keys, with relations such as user.sessions and plan.subscriptions ready for include.

Always generated

POST /auth/login, POST /auth/logout and src/model/User.ts.

User portal

Models

User, Session, Notification, File, SupportTicket, Subscription, Invoice, ApiKey

Routes

Registration, me, refresh tokens, email verification, password reset and change, 2FA, session listing; profile, avatar, settings and account deletion; notifications; file uploads; favourites and bookmarks; search; API keys; billing (plans, subscription, payment methods, invoices); support tickets.

Admin portal

Models

Admin, SupportTicket, AuditLog, Plan, FAQ, Blog, Category, Coupon

Routes

Dashboard stats; user management (suspend, activate, verify, CSV export); analytics; admin management; notification broadcast; audit, activity and security logs; system health; ticket triage; content (FAQs, blog, categories); billing (plans, coupons, subscriptions) — plus /admin/roles when RBAC is on.

Protected routes export middlewares = [requireAuth], or [requireAuth('admin')] with RBAC. A src/migrations/00000000000000_init migration creates every generated table or collection.

CLI Reference

The efc binary comes with express-file-cluster. A global efc run inside a project hands off to the project's own version.

Lifecycle

CommandDescription
efc start devDevelopment server — single process, hot reload.
efc build prodType-check and transpile src/ to dist/src/.
efc start prodRun dist/src/index.js, clustered.
efc run testsVitest.

Code generation

CommandCreates
efc generate route users/[id]A route file with method stubs
efc generate task SendEmailA background task
efc generate middleware authorizeA middleware file
efc generate migration add-ordersA timestamped migration
efc generate seed usersA seed script

Database

CommandDescription
efc db sync [--dry-run]Reconcile tables / collections and indexes with your models.
efc db migrateRun pending migrations as one batch.
efc db rollback [--steps 2]Revert the last batch(es).
efc db statusApplied vs pending migrations.
efc db seed [name]Run all seeds, or one.
efc db studioRead-only database browser on 127.0.0.1.

Diagnostics and tasks

CommandDescription
efc routesThe resolved route table: path, file, methods.
efc tasksRegistered background tasks.
efc tasks failed | retry | purgeInspect and act on dead-lettered jobs.
efc doctorCheck config, environment variables and project setup.
efc --versionInstalled version.

Environment Variables

ignite() doesn't read process.env for configuration — the scaffolded efc.config.ts does, and passes the values in. NODE_ENV is the one variable the framework reads directly.

create-efc-app writes .env (gitignored, secrets filled in) and .env.example (committed, documented placeholders).

VariableRequiredDescription
PORTNo — 3000HTTP listen port
NODE_ENVNodevelopment | production | test. Turns clustering on in production and the dashboard on in development.
DATABASE_URLWith a databasemongodb://… or postgres://…; the scheme picks the engine. Also used by pg-boss and efc db.
JWT_SECRETWith authSigning key — generated by the scaffolder (openssl rand -hex 64).
JWT_EXPIRES_INNo — 7dToken lifetime: 15m, 1h, 7d…
COOKIE_DOMAINNoCookie domain for http-only auth; blank for localhost.
REDIS_URLWith BullMQRedis connection; rediss://, ACL users and /db supported.
CORS_ORIGINSNoComma-separated origins, e.g. http://localhost:3000,https://app.com.
APP_URLWith MailerBase URL for links in emails.
SMTP_HOSTWith Mailersmtp.gmail.com or your SMTP host
SMTP_PORTWith Mailer465 (Gmail, TLS) or typically 587
SMTP_USERWith MailerSending address
SMTP_PASSWith MailerFor Gmail, a 16-character App Password — not your account password
SMTP_FROMNoFrom: address; defaults to SMTP_USER
EFC_DEBUGNo1 prints full stack traces from efc db commands.