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
14 changes: 14 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
**/node_modules
**/dist
**/.astro
.git
.github
**/.env
**/.env.*
!**/.env.example
**/.data
landing
docs
*.png
.playwright-mcp
**/*.log
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ jobs:

- name: Push schema + seed demo data
run: |
pnpm --filter @kinora/server db:push
pnpm --filter @kinora/server migrate latest
pnpm --filter @kinora/server db:seed

- name: E2E (boots server + web, runs viewer + web suites)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ pnpm install
cd packages/server
cp .env.example .env
docker compose up -d # Postgres on :5436
pnpm db:push # create tables
pnpm migrate lastest # create tables
pnpm db:seed # seed a demo account + data, prints login + an API token
pnpm dev # server on :3000

Expand Down
26 changes: 26 additions & 0 deletions packages/server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Build from the REPO ROOT (needs the pnpm workspace + @kinora/core):
# docker build -f packages/server/Dockerfile -t kinora-server .
FROM node:24-alpine AS base
RUN npm i -g corepack@latest && corepack enable
WORKDIR /repo

FROM base AS builder
COPY . .
# Install only the server + its workspace deps (@kinora/core).
RUN pnpm install --frozen-lockfile --filter @kinora/server...
# tsdown bundles @kinora/core into dist, so the prod image needs no workspace lib.
RUN pnpm --filter @kinora/server build
# Self-contained package + prod node_modules -> /prod.
RUN pnpm deploy --filter=@kinora/server --prod /prod

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 kinora
COPY --from=builder --chown=kinora:nodejs /prod /app
USER kinora
EXPOSE 3000

CMD ["node", "dist/src/index.mjs"]
12 changes: 0 additions & 12 deletions packages/server/drizzle.config.ts

This file was deleted.

13 changes: 0 additions & 13 deletions packages/server/drizzle.test.config.ts

This file was deleted.

173 changes: 173 additions & 0 deletions packages/server/migrations/20260609231021_initial_schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import type { Knex } from 'knex'

export async function up(knex: Knex): Promise<void> {
await knex.schema
.createTable('user', (t) => {
t.text('id').primary()
t.text('name').notNullable()
t.text('email').notNullable()
t.boolean('email_verified').notNullable().defaultTo(false)
t.text('image')
t.timestamp('created_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
t.timestamp('updated_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
t.unique(['email'], { indexName: 'user_email_unique' })
})
.createTable('verification', (t) => {
t.text('id').primary()
t.text('identifier').notNullable()
t.text('value').notNullable()
t.timestamp('expires_at', { useTz: false }).notNullable()
t.timestamp('created_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
t.timestamp('updated_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
t.index(['identifier'], 'verification_identifier_idx')
})
.createTable('apikey', (t) => {
t.text('id').primary()
t.text('config_id').notNullable().defaultTo('default')
t.text('name')
t.text('start')
t.text('prefix')
t.text('key').notNullable()
t.text('reference_id').notNullable()
t.integer('refill_interval')
t.integer('refill_amount')
t.timestamp('last_refill_at', { useTz: false })
t.boolean('enabled').defaultTo(true)
t.boolean('rate_limit_enabled').defaultTo(true)
t.integer('rate_limit_time_window').defaultTo(86400000)
t.integer('rate_limit_max').defaultTo(10)
t.integer('request_count').defaultTo(0)
t.integer('remaining')
t.timestamp('last_request', { useTz: false })
t.timestamp('expires_at', { useTz: false })
t.timestamp('created_at', { useTz: false }).notNullable()
t.timestamp('updated_at', { useTz: false }).notNullable()
t.text('permissions')
t.text('metadata')
t.index(['config_id'], 'apikey_configId_idx')
t.index(['reference_id'], 'apikey_referenceId_idx')
t.index(['key'], 'apikey_key_idx')
})
.createTable('account', (t) => {
t.text('id').primary()
t.text('account_id').notNullable()
t.text('provider_id').notNullable()
t.text('user_id').notNullable().references('id').inTable('user').onDelete('CASCADE')
t.text('access_token')
t.text('refresh_token')
t.text('id_token')
t.timestamp('access_token_expires_at', { useTz: false })
t.timestamp('refresh_token_expires_at', { useTz: false })
t.text('scope')
t.text('password')
t.timestamp('created_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
t.timestamp('updated_at', { useTz: false }).notNullable()
t.index(['user_id'], 'account_userId_idx')
})
.createTable('session', (t) => {
t.text('id').primary()
t.timestamp('expires_at', { useTz: false }).notNullable()
t.text('token').notNullable()
t.timestamp('created_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
t.timestamp('updated_at', { useTz: false }).notNullable()
t.text('ip_address')
t.text('user_agent')
t.text('user_id').notNullable().references('id').inTable('user').onDelete('CASCADE')
t.unique(['token'], { indexName: 'session_token_unique' })
t.index(['user_id'], 'session_userId_idx')
})
.createTable('project', (t) => {
t.text('id').primary()
t.text('user_id').notNullable().references('id').inTable('user').onDelete('CASCADE')
t.text('slug').notNullable()
t.text('name').notNullable()
t.text('description')
t.timestamp('created_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
t.timestamp('updated_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
t.unique(['user_id', 'slug'], { indexName: 'project_user_slug_uniq' })
t.index(['user_id'], 'project_userId_idx')
})
.createTable('run', (t) => {
t.text('id').primary()
t.text('project_id').notNullable().references('id').inTable('project').onDelete('CASCADE')
t.timestamp('started_at', { useTz: false }).notNullable()
t.integer('duration').notNullable()
t.jsonb('counts').notNullable()
t.jsonb('counts_by_tag').notNullable().defaultTo(knex.raw(`'{}'::jsonb`))
t.text('playwright_version')
t.jsonb('git')
t.jsonb('ci')
t.integer('shards')
t.timestamp('created_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
t.index(['project_id'], 'run_projectId_idx')
})
.createTable('test', (t) => {
t.text('id').primary()
t.text('run_id').notNullable().references('id').inTable('run').onDelete('CASCADE')
t.text('project_id').notNullable().references('id').inTable('project').onDelete('CASCADE')
t.text('test_key').notNullable()
t.text('title').notNullable()
t.jsonb('title_path').notNullable()
t.text('file').notNullable()
t.integer('line').notNullable()
t.integer('column').notNullable()
t.text('project_name').notNullable()
t.text('status').notNullable()
t.boolean('ok').notNullable()
t.integer('duration').notNullable()
t.integer('retries').notNullable()
t.jsonb('tags').notNullable()
t.jsonb('annotations').notNullable()
t.jsonb('errors').notNullable()
t.jsonb('attachments').notNullable()
t.timestamp('created_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
t.index(['run_id'], 'test_runId_idx')
t.index(['project_id', 'test_key'], 'test_project_key_idx')
})
.createTable('artifact', (t) => {
t.text('id').primary()
t.text('project_id').notNullable().references('id').inTable('project').onDelete('CASCADE')
t.text('run_id').notNullable().references('id').inTable('run').onDelete('CASCADE')
t.text('test_id').references('id').inTable('test').onDelete('CASCADE')
t.text('name').notNullable()
t.text('content_type').notNullable()
t.text('storage_key').notNullable()
t.text('sha1')
t.integer('size')
t.timestamp('created_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
t.index(['run_id'], 'artifact_runId_idx')
})
.createTable('subscription', (t) => {
t.text('user_id').primary().references('id').inTable('user').onDelete('CASCADE')
t.text('polar_customer_id').notNullable()
t.text('tier').notNullable().defaultTo('free')
t.text('status')
t.text('product_id')
t.timestamp('current_period_end', { useTz: false })
t.boolean('cancel_at_period_end').notNullable().defaultTo(false)
t.timestamp('updated_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
})
.createTable('slack_integration', (t) => {
t.text('project_id').primary().references('id').inTable('project').onDelete('CASCADE')
t.text('webhook_url').notNullable()
t.text('policy').notNullable().defaultTo('on-failure')
t.boolean('enabled').notNullable().defaultTo(true)
t.timestamp('created_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
t.timestamp('updated_at', { useTz: false }).notNullable().defaultTo(knex.fn.now())
})
}

export async function down(knex: Knex): Promise<void> {
await knex.schema
.dropTableIfExists('slack_integration')
.dropTableIfExists('subscription')
.dropTableIfExists('artifact')
.dropTableIfExists('test')
.dropTableIfExists('run')
.dropTableIfExists('project')
.dropTableIfExists('session')
.dropTableIfExists('account')
.dropTableIfExists('apikey')
.dropTableIfExists('verification')
.dropTableIfExists('user')
}
6 changes: 2 additions & 4 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@
"build": "tsdown",
"typecheck": "tsc --noEmit",
"test:integration": "vitest run",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:seed": "tsx scripts/seed.ts",
"db:seed:market": "tsx scripts/seed-market.ts",
"migrate": "tsx scripts/migrate.ts",
"purge-expired-runs": "tsx scripts/purge-expired-runs.ts"
},
"dependencies": {
Expand All @@ -38,6 +36,7 @@
"dotenv": "^17.3.1",
"drizzle-orm": "^0.45.2",
"hono": "^4.12.23",
"knex": "^3.2.10",
"pg": "^8.20.0",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
Expand All @@ -46,7 +45,6 @@
"devDependencies": {
"@types/node": "^24.12.3",
"@types/pg": "^8.20.0",
"drizzle-kit": "^0.31.10",
"tsdown": "^0.22.2",
"tsx": "^4.22.4",
"typescript": "~6.0.2",
Expand Down
Loading