Skip to content

Commit 2889fb3

Browse files
committed
feat: add SQLite support for meta database and update Docker configuration
1 parent 6fca7ff commit 2889fb3

14 files changed

Lines changed: 226 additions & 73 deletions

File tree

Dockerfile.backend

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,27 @@
1-
FROM node:24-bookworm-slim AS deps
1+
FROM node:24-bookworm-slim AS frontend-build
2+
3+
WORKDIR /app
4+
5+
RUN corepack enable
6+
7+
ARG VITE_LOCKED_SERVER_URL=/
8+
ARG VITE_LOCKED_SERVER_NAME=StarQuery Hosted
9+
ENV VITE_LOCKED_SERVER_URL=$VITE_LOCKED_SERVER_URL
10+
ENV VITE_LOCKED_SERVER_NAME=$VITE_LOCKED_SERVER_NAME
11+
12+
COPY .npmrc .npmrc
13+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
14+
COPY packages/frontend/package.json packages/frontend/package.json
15+
16+
RUN pnpm install --filter frontend... --frozen-lockfile
17+
18+
COPY packages/frontend packages/frontend
19+
20+
WORKDIR /app/packages/frontend
21+
22+
RUN pnpm build
23+
24+
FROM node:24-bookworm-slim AS backend-deps
225

326
WORKDIR /app
427

@@ -7,6 +30,7 @@ RUN apt-get update \
730
&& apt-get install -y --no-install-recommends python3 make g++ \
831
&& rm -rf /var/lib/apt/lists/*
932

33+
COPY .npmrc .npmrc
1034
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
1135
COPY packages/backend/package.json packages/backend/package.json
1236

@@ -16,16 +40,26 @@ FROM node:24-bookworm-slim
1640

1741
WORKDIR /app
1842

19-
RUN corepack enable
20-
21-
COPY --from=deps /app/node_modules ./node_modules
22-
COPY --from=deps /app/package.json ./package.json
23-
COPY --from=deps /app/pnpm-lock.yaml ./pnpm-lock.yaml
24-
COPY --from=deps /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
43+
COPY --from=backend-deps /app/node_modules ./node_modules
44+
COPY --from=backend-deps /app/package.json ./package.json
45+
COPY --from=backend-deps /app/pnpm-lock.yaml ./pnpm-lock.yaml
46+
COPY --from=backend-deps /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
2547
COPY packages/backend packages/backend
48+
COPY --from=frontend-build /app/packages/frontend/dist /app/packages/frontend/dist
49+
50+
RUN mkdir -p /var/lib/starquery
2651

2752
WORKDIR /app/packages/backend
2853

29-
EXPOSE 3000
54+
ENV NODE_ENV=production
55+
ENV HOST=0.0.0.0
56+
ENV PORT=8080
57+
ENV STARQUERY_MODE=hosted
58+
ENV STARQUERY_SERVER_NAME="StarQuery Hosted"
59+
ENV STARQUERY_META_DRIVER=sqlite
60+
ENV STARQUERY_META_SQLITE_PATH=/var/lib/starquery/starquery-meta.sqlite
61+
ENV STARQUERY_FRONTEND_DIST_PATH=/app/packages/frontend/dist
62+
63+
EXPOSE 8080
3064

31-
CMD ["pnpm", "start"]
65+
CMD ["node", "--import", "tsx", "src/index.ts"]

README.md

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@
22

33
StarQuery is a desktop and web database/resource browser with support for SQL datasources, Elasticsearch, and S3-compatible object storage.
44

5+
## Docker Quick Start
6+
7+
Run the prebuilt single-image deployment:
8+
9+
```bash
10+
docker run -it --rm \
11+
-p 8080:8080 \
12+
--add-host=host.docker.internal:host-gateway \
13+
-v starquery-data:/var/lib/starquery \
14+
interaapps/starquery
15+
```
16+
17+
Then open:
18+
19+
- App: `http://localhost:8080`
20+
21+
Networking note:
22+
- If StarQuery inside the container should connect to services running on your host machine, use `host.docker.internal` as the host in your datasource config instead of `localhost`.
23+
- The `--add-host=host.docker.internal:host-gateway` flag makes that work on Linux as well.
24+
525
Status:
626
- Work in progress
727
- No stable releases yet
@@ -39,7 +59,8 @@ Runtime targets:
3959

4060
- Intended for self-hosting on a server
4161
- Auth is enabled by default
42-
- Uses MySQL as the default meta database
62+
- Uses MySQL as the default meta database in the generic backend config
63+
- The published Docker image uses SQLite for the meta database by default so no extra DB is required
4364
- Can optionally bootstrap users, projects, and datasources from JSON
4465

4566
### Plain web frontend
@@ -73,9 +94,10 @@ Use `docker compose down -v` if you want to wipe the database and re-run the see
7394
## Hosted Docker Deployment
7495

7596
There is a dedicated Docker deployment for the hosted web version with:
76-
- a Vite-built frontend served by Nginx
77-
- a Node backend
78-
- a MySQL metastore for users, roles, projects, datasources, tokens, and settings
97+
- one Node app image that serves both the built frontend and the backend API
98+
- a SQLite metastore by default, stored in a Docker volume
99+
100+
This means the default self-hosted setup does not require any separate database container or database configuration.
79101

80102
Start it with:
81103

@@ -85,12 +107,21 @@ docker compose -f docker-compose.hosted.yml up -d --build
85107

86108
Then open:
87109

88-
- Frontend: `http://localhost:8080`
110+
- App: `http://localhost:8080`
111+
112+
You can also run the single image directly without compose:
113+
114+
```bash
115+
docker build -t starquery -f Dockerfile.backend .
116+
docker run -it --rm \
117+
-p 8080:8080 \
118+
--add-host=host.docker.internal:host-gateway \
119+
-v starquery-data:/var/lib/starquery \
120+
starquery
121+
```
89122

90123
Services in the hosted stack:
91-
- `frontend`: public web UI on port `8080`
92-
- `backend`: internal StarQuery API in `hosted` mode
93-
- `meta-db`: MySQL database used by the backend as its metastore
124+
- `app`: public StarQuery web UI and backend API on port `8080`
94125

95126
Stop it with:
96127

@@ -104,6 +135,14 @@ Reset the hosted metastore volume with:
104135
docker compose -f docker-compose.hosted.yml down -v
105136
```
106137

138+
The hosted compose file now uses:
139+
- `STARQUERY_META_DRIVER=sqlite`
140+
- `STARQUERY_META_SQLITE_PATH=/var/lib/starquery/starquery-meta.sqlite`
141+
- `VITE_LOCKED_SERVER_URL=/`
142+
- `PORT=8080`
143+
144+
If you prefer MySQL for the metastore, you can still switch the backend to MySQL by providing the normal `STARQUERY_META_MYSQL_*` environment variables in your own compose override.
145+
107146
## Configuration Overview
108147

109148
There are three main configuration surfaces:
@@ -145,7 +184,7 @@ These are read by the StarQuery backend.
145184

146185
| Variable | Default | Description |
147186
| --- | --- | --- |
148-
| `STARQUERY_META_DRIVER` | `mysql` in hosted mode, `sqlite` in local mode | Meta database driver. Valid values: `mysql`, `sqlite`. |
187+
| `STARQUERY_META_DRIVER` | `mysql` in hosted mode, `sqlite` in local mode | Meta database driver. Valid values: `mysql`, `sqlite`. The bundled Docker hosted deployment overrides this to `sqlite` by default so no extra DB is required. |
149188
| `STARQUERY_META_SQLITE_PATH` | `<cwd>/.starquery/starquery-meta.sqlite` | SQLite file path for the meta database. Only used when `STARQUERY_META_DRIVER=sqlite`. |
150189
| `STARQUERY_META_MYSQL_HOST` | `127.0.0.1` | MySQL host for the meta database. |
151190
| `STARQUERY_META_MYSQL_PORT` | `3307` | MySQL port for the meta database. |

docker-compose.hosted.yml

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,23 @@
11
services:
2-
frontend:
3-
build:
4-
context: .
5-
dockerfile: Dockerfile.frontend
6-
depends_on:
7-
- backend
8-
ports:
9-
- '8080:80'
10-
restart: unless-stopped
11-
12-
backend:
2+
app:
133
build:
144
context: .
155
dockerfile: Dockerfile.backend
16-
depends_on:
17-
meta-db:
18-
condition: service_healthy
6+
args:
7+
VITE_LOCKED_SERVER_URL: /
8+
VITE_LOCKED_SERVER_NAME: StarQuery Hosted
199
environment:
2010
HOST: 0.0.0.0
21-
PORT: 3000
11+
PORT: 8080
2212
STARQUERY_MODE: hosted
2313
STARQUERY_SERVER_NAME: StarQuery Hosted
24-
STARQUERY_META_DRIVER: mysql
25-
STARQUERY_META_MYSQL_HOST: meta-db
26-
STARQUERY_META_MYSQL_PORT: 3306
27-
STARQUERY_META_MYSQL_USER: starquery
28-
STARQUERY_META_MYSQL_PASSWORD: starquery
29-
STARQUERY_META_MYSQL_DATABASE: starquery
30-
expose:
31-
- '3000'
32-
restart: unless-stopped
33-
34-
meta-db:
35-
image: mysql:8.4
36-
environment:
37-
MYSQL_ROOT_PASSWORD: root
38-
MYSQL_DATABASE: starquery
39-
MYSQL_USER: starquery
40-
MYSQL_PASSWORD: starquery
14+
STARQUERY_META_DRIVER: sqlite
15+
STARQUERY_META_SQLITE_PATH: /var/lib/starquery/starquery-meta.sqlite
16+
ports:
17+
- '8080:8080'
4118
volumes:
42-
- starquery-meta-data:/var/lib/mysql
43-
healthcheck:
44-
test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD']
45-
interval: 10s
46-
timeout: 5s
47-
retries: 12
48-
start_period: 20s
19+
- starquery-app-data:/var/lib/starquery
4920
restart: unless-stopped
5021

5122
volumes:
52-
starquery-meta-data:
23+
starquery-app-data:

packages/backend/src/auth/cors.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ test('hosted CORS allows the configured public origin', async () => {
5959
assert.equal(await evaluateOrigin(config, 'https://evil.example.com'), false)
6060
})
6161

62+
test('hosted CORS allows every origin when wildcard is configured', async () => {
63+
const config = createConfig({ corsAllowedOrigins: ['*'] })
64+
assert.equal(await evaluateOrigin(config, 'http://localhost:5173'), true)
65+
assert.equal(await evaluateOrigin(config, 'https://evil.example.com'), true)
66+
})
67+
6268
test('local mode keeps broad CORS for the desktop app flow', () => {
6369
const config = createConfig({ mode: 'local' })
6470
const options = createCorsOptions(config)

packages/backend/src/auth/cors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export function createCorsOptions(config: AppConfig): CorsOptions {
1616
}
1717
}
1818

19+
const allowAnyOrigin = config.corsAllowedOrigins.some((origin) => origin.trim() === '*')
1920
const allowedOrigins = new Set(
2021
[
2122
...config.corsAllowedOrigins.map((origin) => normalizeOrigin(origin)).filter((origin): origin is string => Boolean(origin)),
@@ -25,6 +26,11 @@ export function createCorsOptions(config: AppConfig): CorsOptions {
2526

2627
return {
2728
origin(origin, callback) {
29+
if (allowAnyOrigin) {
30+
callback(null, true)
31+
return
32+
}
33+
2834
if (!origin) {
2935
callback(null, true)
3036
return

packages/backend/src/config/app-config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type AppConfig = {
88
host: string
99
publicUrl?: string
1010
corsAllowedOrigins: string[]
11+
frontendDistPath?: string
1112
serverName: string
1213
mode: AppMode
1314
requestBodyLimit: string
@@ -63,6 +64,8 @@ export function loadAppConfig(): AppConfig {
6364
.split(',')
6465
.map((entry) => entry.trim())
6566
.filter(Boolean),
67+
frontendDistPath:
68+
process.env.STARQUERY_FRONTEND_DIST_PATH?.trim() || path.resolve(process.cwd(), '..', 'frontend', 'dist'),
6669
serverName:
6770
process.env.STARQUERY_SERVER_NAME ?? (mode === 'hosted' ? 'Hosted Server' : 'Local Computer'),
6871
mode,

packages/backend/src/meta/connection.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,41 +33,59 @@ function normalizeSqliteRows(rows: unknown[]) {
3333
})
3434
}
3535

36+
function normalizeSqliteParam(param: unknown) {
37+
if (typeof param === 'boolean') {
38+
return param ? 1 : 0
39+
}
40+
41+
if (param instanceof Date) {
42+
return param.toISOString()
43+
}
44+
45+
return param
46+
}
47+
48+
function normalizeSqliteParams(params: unknown[]) {
49+
return params.map((param) => normalizeSqliteParam(param))
50+
}
51+
3652
function normalizeSqliteRow(row: unknown) {
3753
return normalizeSqliteRows([row])[0] ?? []
3854
}
3955

4056
function createSqliteProxyCallback(connection: SqliteConnection) {
4157
return async (query: string, params: unknown[], method: SqliteMethod) => {
4258
const statement = connection.prepare(query)
59+
const normalizedParams = normalizeSqliteParams(params)
4360

4461
if (method === 'run') {
45-
statement.run(...params)
62+
statement.run(...normalizedParams)
4663
return { rows: [] }
4764
}
4865

4966
if (method === 'get') {
50-
const row = statement.get(...params)
67+
const row = statement.get(...normalizedParams)
5168
return { rows: row === undefined ? [] : [normalizeSqliteRow(row)] }
5269
}
5370

54-
const rows = statement.all(...params)
71+
const rows = statement.all(...normalizedParams)
5572
return { rows: normalizeSqliteRows(rows as unknown[]) }
5673
}
5774
}
5875

5976
function executeSqlite(connection: SqliteConnection, statement: string, params: unknown[] = []) {
6077
const prepared = connection.prepare(statement)
78+
const normalizedParams = normalizeSqliteParams(params)
6179
const normalizedStatement = statement.trim().toLowerCase()
6280
if (
6381
normalizedStatement.startsWith('select') ||
6482
normalizedStatement.startsWith('pragma') ||
6583
normalizedStatement.startsWith('with')
6684
) {
67-
return prepared.all(...params) as unknown[]
85+
return prepared.all(...normalizedParams) as unknown[]
6886
}
6987

70-
prepared.run(...params)
88+
prepared.run(...normalizedParams)
7189
return []
7290
}
7391

packages/backend/src/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { registerAuthRoutes } from './routes/auth-routes.ts'
1414
import { registerProjectRoutes } from './routes/projects-routes.ts'
1515
import { registerServerRoutes } from './routes/server-routes.ts'
1616
import { registerSourceRoutes } from './routes/source-routes.ts'
17+
import { registerStaticFrontend } from './static-frontend.ts'
1718

1819
export type StartBackendServerOptions = Partial<Omit<AppConfig, 'metaStore' | 'auth'>> & {
1920
auth?: Partial<Omit<AppConfig['auth'], 'seedAdmin' | 'openId'>> & {
@@ -76,6 +77,7 @@ export async function startBackendServer(overrides: StartBackendServerOptions =
7677
registerAdminRoutes(app, context)
7778
registerProjectRoutes(app, context)
7879
registerSourceRoutes(app, context)
80+
registerStaticFrontend(app, config)
7981

8082
const server = await new Promise<Server>((resolve, reject) => {
8183
const httpServer = app.listen(config.port, config.host, () => resolve(httpServer))

0 commit comments

Comments
 (0)