Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .devcontainer/devcontainer.env_example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
NEXTAUTH_SECRET=SOME_TOKEN_FOR_NEXTJS_AUTHENTICATION
DATABASE_URL="file:/workspace/storage/db/data.db"
109 changes: 109 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# QuickStack AI Coding Instructions

QuickStack is a self-hosted PaaS built with Next.js 14 (App Router) that manages Kubernetes (k3s) deployments. It uses a custom server (`src/server.ts`) that wraps Next.js to handle WebSockets for terminal streaming.

## Architecture Overview

### Three-Layer Structure
- **`src/app/`** - Next.js App Router pages and Server Actions (all pages use `'use server'`)
- **`src/server/`** - Backend services that interact with Kubernetes and database
- **`src/shared/`** - Shared models, utils, and Zod schemas (used by both frontend and server)

### Key Adapters (`src/server/adapter/`)
- `kubernetes-api.adapter.ts` - Wraps `@kubernetes/client-node` APIs (`k3s.core`, `k3s.apps`, etc.)
- `db.client.ts` - Prisma singleton (`dataAccess.client`)
- `longhorn-api.adapter.ts` - Longhorn storage API

### Service Pattern
Services are singleton classes exported as default instances:
```typescript
class AppService { /* methods */ }
const appService = new AppService();
export default appService;
```

## Server Actions Pattern

All server actions use wrappers from `src/server/utils/action-wrapper.utils.ts`:

```typescript
// For form submissions with Zod validation
export const saveApp = async (data: AppModel) =>
saveFormAction(data, AppModelSchema, async (validated) => {
await appService.save(validated);
return new SuccessActionResult(undefined, 'App saved');
}) as Promise<ServerActionResult<any, void>>;

// For simple actions without form validation
export const deleteApp = async (id: string) =>
simpleAction(async () => {
await isAuthorizedWriteForApp(id); // Auth check
await appService.deleteById(id);
return new SuccessActionResult(undefined, 'App deleted');
});
```

## Database & Prisma

- SQLite database at `storage/db/data.db`
- Schema: `prisma/schema.prisma`
- Zod schemas auto-generated to `src/shared/model/generated-zod/`
- After schema changes: `yarn prisma-migrate` (runs `prisma migrate dev` + fixes Zod imports)
- Use `dataAccess.client` for queries, supports transactions via `$transaction()`

## Kubernetes Naming Conventions

Use `KubeObjectNameUtils` for consistent k8s object names:
- `toProjectId(name)` → `proj-{name}-{hash}`
- `toAppId(name)` → `app-{name}-{hash}`
- `toPvcName(volumeId)` → `pvc-{volumeId}`
- `toServiceName(appId)` → `svc-{appId}`

## Frontend Patterns

### State Management
Zustand stores in `src/frontend/states/zustand.states.ts`:
- `useConfirmDialog` - Promise-based confirmation dialogs
- `useInputDialog` - Promise-based input dialogs
- `useBreadcrumbs` - Page breadcrumb navigation

### UI Components
- shadcn/ui components in `src/components/ui/`
- Custom components in `src/components/custom/`
- Forms use `react-hook-form` with Zod resolvers

### Real-time Updates
- Socket.IO server at `/pod-terminal` namespace for terminal streaming
- WebSocket server for live pod logs

## Caching

Next.js `unstable_cache` with tag-based invalidation:
```typescript
// Reading with cache
await unstable_cache(
async () => dataAccess.client.app.findMany({ where: { projectId } }),
[Tags.apps(projectId)],
{ tags: [Tags.apps(projectId)] }
)(projectId);

// Invalidating after mutations
revalidateTag(Tags.apps(projectId));
```

## Testing

- Jest with jsdom environment
- Tests in `src/__tests__/{frontend,server,shared}/`
- Path alias `@/` maps to `src/`
- Run: `yarn test`

## Development Setup

1. Use provided devcontainer (includes Node, Bun, Prisma extension)
2. Provide k3s credentials in `kube-config.config` at project root
3. `yarn install` → `yarn dev` for Next.js or `yarn dev-live` for custom server

## Commit Convention

Follow Conventional Commits: `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `chore:`
6 changes: 5 additions & 1 deletion .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
release:
types: [released, prereleased]

env:
# dummy database url for build time --> prisma
DATABASE_URL: file:./dev.db

permissions:
contents: read

Expand All @@ -17,7 +21,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: '18.x'
node-version: '22.x'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies for backend
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/canary-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
- main
workflow_dispatch:

env:
# dummy database url for build time --> prisma
DATABASE_URL: file:./dev.db

permissions:
contents: read

Expand All @@ -18,7 +22,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: '18.x'
node-version: '22.x'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies for backend
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
branches:
- main

env:
# dummy database url for build time --> prisma
DATABASE_URL: file:./dev.db

permissions:
contents: read

Expand All @@ -18,7 +22,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: '18.x'
node-version: '22.x'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies for backend
Expand Down
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:18-alpine AS base
FROM node:22-alpine AS base

ARG VERSION_ARG
RUN apk add --no-cache openssl
Expand All @@ -20,6 +20,9 @@ WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Dummy Database URL for Prisma generation
ENV DATABASE_URL="file:./dev.db"

RUN yarn run prisma-generate-build
RUN yarn run build
RUN rm -rf ./next/standalone
Expand All @@ -31,6 +34,7 @@ WORKDIR /app
ENV NODE_ENV=production
ENV PYTHON=/usr/bin/python3
ENV QS_VERSION=$VERSION_ARG
ENV DATABASE_URL="file:/app/storage/db/data.db"

RUN apk add --no-cache git

Expand All @@ -42,6 +46,7 @@ RUN chown nextjs:nodejs storage tmp-storage

COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
COPY --from=builder --chown=nextjs:nodejs /app/prisma.config.ts ./prisma.config.js
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ QuickStack is a self-hosted PaaS solution designed to simplify the management of

Developed as part of a student project by [glueh-wyy-huet](https://github.com/glueh-wyy-huet) and [biersoeckli](https://github.com/biersoeckli) at the [Eastern Switzerland University of Applied Sciences](https://ost.ch/), QuickStack provides a scalable and cost-effective alternative to commercial cloud PaaS offerings like Vercel, Digital Ocean App Platform or Azure App Service.

<img src="/github-assets/app-settings-general.png" alt="QuickStack App Settings Image" width="100%" />

## Key Features

* **One-Command Installation:** Deploy QuickStack on a VPS with a single command.
Expand Down
Binary file added github-assets/app-settings-general.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"@hookform/resolvers": "^3.9.0",
"@kubernetes/client-node": "^0.22.2",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.21.1",
"@prisma/adapter-better-sqlite3": "^7.1.0",
"@prisma/client": "7.1.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
Expand All @@ -40,10 +41,6 @@
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.4",
"@tanstack/react-table": "^8.20.5",
"@types/bcrypt": "^5.0.2",
"@types/node-schedule": "^2.1.7",
"@types/qrcode": "^1.5.5",
"@types/ws": "^8.5.13",
"@xterm/xterm": "^5.5.0",
"bcrypt": "^5.1.1",
"bufferutil": "^4.0.9",
Expand All @@ -53,14 +50,15 @@
"cross-env": "^7.0.3",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dotenv": "^17.2.3",
"lucide-react": "^0.465.0",
"moment": "^2.30.1",
"next": "14.2.15",
"next-auth": "^4.24.8",
"next-themes": "^0.3.0",
"node-schedule": "^2.1.1",
"otpauth": "^9.3.4",
"prisma": "^5.21.1",
"prisma": "7.1.0",
"qrcode": "^1.5.4",
"react": "^18.3.1",
"react-day-picker": "8.10.1",
Expand All @@ -82,8 +80,13 @@
},
"devDependencies": {
"@testing-library/dom": "^10.4.0",
"@types/bcrypt": "^5.0.2",
"@types/node-schedule": "^2.1.7",
"@types/qrcode": "^1.5.5",
"@types/ws": "^8.5.13",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@types/better-sqlite3": "^7.6.13",
"@types/jest": "^29.5.14",
"@types/mocha": "^10.0.10",
"@types/node": "^22.7.9",
Expand Down
12 changes: 12 additions & 0 deletions prisma.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config'

export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
})
3 changes: 1 addition & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
provider = "prisma-client-js"
provider = "prisma-client-js" // TODO: Upgrade to use native js client in prisma 7
}

generator zod {
Expand All @@ -32,7 +32,6 @@ generator zod {

datasource db {
provider = "sqlite"
url = "file:../storage/db/data.db"
}

// *** The following code is for the default NextAuth.js schema
Expand Down
16 changes: 12 additions & 4 deletions src/server/adapter/db.client.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import "dotenv/config";
import { Prisma, PrismaClient } from "@prisma/client";
import { DefaultArgs } from "@prisma/client/runtime/library";
import { ListUtils } from "../../shared/utils/list.utils";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { DefaultArgs } from "@prisma/client/runtime/client";

type clientType = keyof PrismaClient<Prisma.PrismaClientOptions, never | undefined>;

const prismaClientSingleton = () => {
return new PrismaClient()
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL is not defined in environment variables');
}
const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL,
});
return new PrismaClient({ adapter });
}

declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;

const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
Expand Down Expand Up @@ -42,7 +50,7 @@ export class DataAccessClient {
});
}));
}
}
}

async getById(clientType: clientType, id: any, idKey = 'id') {
return await (this.client[clientType] as any).findFirstOrThrow({
Expand Down
1 change: 0 additions & 1 deletion src/server/services/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { revalidateTag, unstable_cache } from "next/cache";
import dataAccess from "../adapter/db.client";
import { Tags } from "../utils/cache-tag-generator.utils";
import { App, AppBasicAuth, AppDomain, AppFileMount, AppPort, AppVolume, Prisma } from "@prisma/client";
import { DefaultArgs } from "@prisma/client/runtime/library";
import { AppExtendedModel, AppWithProjectModel } from "@/shared/model/app-extended.model";
import { ServiceException } from "@/shared/model/service.exception.model";
import { KubeObjectNameUtils } from "../utils/kube-object-name.utils";
Expand Down
Loading