---
title: "Frameworks and ORMs"
description: "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.

```hcl specific.hcl
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:

```hcl specific.hcl
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" {}
```

```prisma prisma/schema.prisma
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](/guides/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:

```bash
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:

```hcl specific.hcl
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"
  }
}
```

```typescript drizzle.config.ts
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`.

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

Specific recommends [Reshape](/guides/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:

```bash
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
```

```hcl specific.hcl
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](/guides/builds) for the managed base images and [Services](/guides/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.
