Skip to content
Specific Docs
Esc
navigateopen⌘Jpreview
On this page

Frameworks and ORMs

Specific runs any language and framework through plain environment variables. This page covers the few tools with a known trap: Next.js, Prisma, Drizzle, and Python virtual environments.

Specific has no SDK and no framework adapters. A service is a build and a command, and everything it needs (ports, database URLs, secrets) arrives as environment variables. That is enough for almost any stack, and your coding agent will usually get a new framework running without help.

A few tools have a step that is easy to get wrong the first time. Each section below covers one of them: the working configuration, and the trap it avoids.

Next.js

A Next.js app is one service that builds with next build and serves with next start. Next.js reads PORT from the environment, so the same commands work under specific dev and in production.

build "web" {
  base    = "node"
  command = "npm run build"
}

service "web" {
  build   = build.web
  command = "npm start"

  endpoint {
    public = true
  }

  env = {
    PORT         = port
    DATABASE_URL = postgres.main.url
  }

  dev {
    command = "npm run dev"
  }
}

postgres "main" {}

The trap: environment variables during the build. next build executes application code to pre-render pages. Values from specific.hcl, such as DATABASE_URL or secrets, are not available during the build, so a page that connects to the database at build time fails the build. Make sure that code runs at request time instead:

  • Mark pages and layouts that need the database as dynamic (export const dynamic = "force-dynamic"), or fetch inside route handlers and server actions rather than at module scope.
  • Create database clients lazily, inside the function that uses them, not at import time.

NEXT_PUBLIC_* variables are inlined into the client bundle at build time. They must be fixed values (a committed .env file or a string literal in the build’s env block), never references to secrets or configs.

Prisma

Generate the client during the build, run migrations in a pre_deploy hook, and give Prisma both connection strings:

build "api" {
  base    = "node"
  command = "npx prisma generate && npm run build"
}

service "api" {
  build   = build.api
  command = "node dist/index.js"

  endpoint {
    public = true
  }

  env = {
    PORT         = port
    DATABASE_URL = postgres.main.url
    DIRECT_URL   = postgres.main.direct_url
  }

  pre_deploy {
    command = "npx prisma migrate deploy"
  }

  dev {
    command = "npm run dev"
  }
}

postgres "main" {}
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}

The traps. Without prisma generate in the build command, the client does not exist when your code compiles. Without DIRECT_URL, migrations run through the connection pooler, which Prisma’s migration engine does not support; postgres.<name>.direct_url bypasses it (see Postgres). And a pre_deploy hook is what makes a failed migration stop the deploy before new code serves traffic.

In development, run Prisma commands through specific exec so they see the same DATABASE_URL as the running service:

specific exec api -- npx prisma migrate dev --name add_users_table
specific exec api -- npx prisma db seed

Reuse one PrismaClient instance (the usual globalThis singleton) so hot reloading does not open a new connection pool on every change. You do not need Prisma Studio: the local dashboard that specific dev serves has a database viewer and editor.

Drizzle

Read the connection string from the environment, and decide up front how migrations run:

build "api" {
  base    = "node"
  command = "npm run build"
}

postgres "main" {}

service "api" {
  build   = build.api
  command = "node dist/index.js"

  endpoint {
    public = true
  }

  env = {
    PORT         = port
    DATABASE_URL = postgres.main.url
  }

  dev {
    command = "npm run dev"
  }

  pre_deploy {
    command = "npx drizzle-kit migrate"
  }
}
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: { url: process.env.DATABASE_URL! },
});

The trap: push is not migrate. drizzle-kit push applies schema changes straight to the local database and writes no migration files, so a schema change made with push alone never reaches production. Run drizzle-kit generate after every schema change and commit the files; pre_deploy then applies them with migrate.

specific exec api -- npx drizzle-kit push      # local iteration only
specific exec api -- npx drizzle-kit generate  # after every schema change

Specific recommends Reshape for schema migrations instead: it applies migrations automatically during specific dev and keeps the old and new schema available while a deploy rolls out. Drizzle then works purely as the query layer. Drizzle Studio is not needed; the local dashboard has a database viewer.

Python

In production, base = "python" builds the image and installs dependencies. In development, specific dev runs your command directly, so it has to point at a virtual environment you created:

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
build "api" {
  base = "python"
}

service "api" {
  build   = build.api
  command = "uvicorn app:app --host 0.0.0.0 --port $PORT"

  endpoint {
    public = true
  }

  env = {
    PORT         = port
    DATABASE_URL = postgres.main.url
  }

  dev {
    command = ".venv/bin/uvicorn app:app --host 0.0.0.0 --port $PORT --reload"
  }
}

postgres "main" {}

The trap: the dev command does not activate anything. Service commands run in a plain shell, so python app.py in dev uses the system Python and misses your dependencies. Use the binary inside the virtual environment (.venv/bin/python, .venv/bin/uvicorn), or source .venv/bin/activate && .... In a monorepo, root sets the working directory and the .venv path is relative to it.

Framework commands that work, production and dev:

  • Flask: flask run --host 0.0.0.0 --port $PORT with FLASK_APP in env; add --reload in dev.
  • FastAPI: uvicorn app:app --host 0.0.0.0 --port $PORT, as above.
  • Django: gunicorn myproject.wsgi --bind 0.0.0.0:$PORT in production, python manage.py runserver 0.0.0.0:$PORT in dev, and python manage.py migrate in a pre_deploy hook.

Imports resolve normally because specific dev sets the working directory to the service’s root (or the project root), so from myapp.utils import ... and python -m myapp both work.

Something else?

Any language works the same way: a build with a base or a Dockerfile, a command, and PORT from the environment. See Builds for the managed base images and Services for endpoints, health checks, and deploy hooks. If your agent hits a repeatable trap with another tool, specific feedback "..." (once enabled in your user settings) is the fastest way to get it onto this page.

Was this page helpful?