A self-hosted Next.js application with a single source of truth for all layers — database, GraphQL API, and React front-end — built on top of the Next.js self-hosting guide.
Every entity in app/db/entities/ is decorated for both TypeORM (database) and TypeGraphQL (API), and the same TypeScript class is imported directly in React components and server actions.
app/db/entities/User.ts
│
├─ @Entity() ──▶ PostgreSQL table (TypeORM)
├─ @ObjectType() ──▶ GraphQL type (TypeGraphQL → Apollo)
└─ import { User } ──▶ React components & server libs
No separate DTO, schema file, or generated code — add a field once and it propagates everywhere.
// app/db/entities/User.ts
@ObjectType('User') // ← GraphQL type (name must be explicit, prod build mangles class names)
@Entity({ name: 'users' }) // ← PostgreSQL table
export class User {
@Field(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@Column({ type: 'varchar' })
login: string;
@Field(() => UserRole)
@Column({ type: 'enum', enum: UserRole })
role: UserRole;
}
@InputType('UsersFilter') // ← GraphQL input for filtering
export class UsersFilter {
@Field(() => String, { nullable: true }) id?: string;
@Field(() => String, { nullable: true }) login?: string;
@Field(() => UserRole, { nullable: true }) role?: UserRole;
}Important:
@ObjectTypeand@InputTypedecorators must always include an explicit string name (e.g.@ObjectType('User')). Next.js production builds mangle class names, so TypeGraphQL's default of usingconstructor.namewould produce broken schemas ("r"instead of"User"). The ESLint rulelocal/require-typegraphql-explicit-nameenforces this.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, standalone output) |
| Database | PostgreSQL + TypeORM 0.3.27 |
| API | GraphQL — Apollo Server 5 + TypeGraphQL 2 |
| Client | Apollo Client 4 + @apollo/client-integration-nextjs |
| Auth | NextAuth 4 (GitHub, Google OAuth + credentials) |
| Runtime | Node.js 20, React 19 |
| CI/CD | GitHub Actions → SSH deploy |
├── app/
│ ├── api/
│ │ ├── auth/ NextAuth route handler
│ │ └── graphql/
│ │ ├── route.ts Apollo handler (GET + POST)
│ │ ├── schema.ts buildGqlSchema()
│ │ └── resolvers/
│ │ ├── ... API crud handlers
│ ├── db/
│ │ ├── entities/ Single source of truth (TypeORM + TypeGraphQL)
│ │ │ ├── ... Entities models
│ │ ├── migrations/ Plain JS migrations (no ts-node in prod)
│ │ ├── db.ts TypeORM DataSource
│ │ └── runMigrations.js
│ ├── libs/ Server-side data fetchers (use Apollo client)
│ ├── providers.tsx Apollo + Auth providers
│ ├── layout.tsx
│ └── ... All pages folders
├── components/ Shared React components
├── server/
│ ├── apollo.ts ApolloServer singleton
│ └── context.ts GraphQL request context (userId from session)
├── utils/
├── docker/
│ ├── development/ compose.yaml (app + postgres)
│ └── production/ Dockerfile (multi-stage) + compose.yaml
├── eslint-rules/ Local ESLint rules
│ └── require-typegraphql-explicit-name.mjs
└── .github/workflows/
└── deploy.yml checks (lint + type-check) → deploy
- Docker + Docker Compose
- Node.js 20+
npm install
npm start # docker compose up (app + postgres)The app runs at http://localhost:3000.
In development mode Apollo Server exposes a GraphQL Sandbox at:
http://localhost:3000/api/graphql
Open it in a browser to explore the schema, run queries, and test mutations interactively.
npm run type-check # tsc --noEmit
npm run lint # eslint .Migrations live in app/db/migrations/ as plain .js files (no ts-node needed in production).
npm run migration:runThe CI/CD pipeline (GitHub Actions) does:
checksjob (runs on every push tomainorstaging):npm run type-checknpm run lint
deployjob (only onmain, only ifcheckspasses):- Builds a Docker image (multi-stage: deps → migrate-deps → builder → runner)
- Copies the image to the server via SCP
- SSH: loads image, starts DB, runs migrations, starts app
ssh root@your_server_ip
curl -o ~/deploy.sh https://raw.githubusercontent.com/4-life/hello-world/main/deploy.sh
chmod +x ~/deploy.sh
./deploy.sh| Name | Kind |
|---|---|
SSH_HOST, SSH_USER, SSH_PORT |
Variables / Secrets |
SSH_PRIVATE_KEY |
Secret |
POSTGRES_USER, POSTGRES_DB |
Variables |
POSTGRES_PASSWORD, NEXTAUTH_SECRET |
Secrets |
CLIENT_ID_GITHUB, CLIENT_SECRET_GITHUB |
Secrets |
CLIENT_ID_GOOGLE, CLIENT_SECRET_GOOGLE |
Secrets |