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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ jobs:
TZ: America/Sao_Paulo

backend-e2e:
name: Backend e2e (PostgreSQL 16, TZ=${{ matrix.tz }})
name: Backend e2e (PostgreSQL, TZ=${{ matrix.tz }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
Expand All @@ -150,8 +150,8 @@ jobs:
tz: [UTC, America/Sao_Paulo]
services:
postgres:
# Same major as docker/docker-compose.yml. PostgreSQL 18 lands in v0.2.
image: postgres:16
# Same image and digest as docker/docker-compose.yml.
image: postgres:18-alpine@sha256:10a5c69a83035ab5239f5620d708520e8b28f5aa1f2b20143e6024532edadfb5
env:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
Expand Down
8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Thanks for your interest in contributing to Thinker! This guide will help you ge
| Layer | Technology |
| -------- | ------------------------------------------------ |
| Frontend | SolidJS, uPlot, custom CSS theme |
| Backend | NestJS 11, TypeORM, PostgreSQL 16 |
| Backend | NestJS 11, TypeORM, PostgreSQL 18 |
| Auth | Better Auth (auto-login on localhost) |
| Routing | OpenAI-compatible proxy (`/v1/chat/completions`) |
| Build | Turborepo + npm workspaces |
Expand Down Expand Up @@ -70,7 +70,7 @@ docker run --name thinker-postgres \
-e POSTGRES_PASSWORD=mypassword \
-e POSTGRES_DB=mydatabase \
-p 5432:5432 \
-d postgres:16
-d postgres:18
```

That container matches the sample `DATABASE_URL` above:
Expand Down Expand Up @@ -154,7 +154,7 @@ The backend runs standalone and OpenClaw talks to it as a regular OpenAI-compati

### Backend (`packages/backend`)

- **Framework**: NestJS 11 with TypeORM 0.3 and PostgreSQL 16
- **Framework**: NestJS 11 with TypeORM 0.3 and PostgreSQL 18
- **Auth**: Better Auth (email/password + Google, GitHub, Discord OAuth)
- **Tests**: Jest for unit tests (`*.spec.ts`), Supertest for e2e tests (`test/`)
- **Key directories**: `entities/` (data models), `analytics/` (dashboard queries), `routing/` (proxy, scoring, tier assignment), `auth/` (session management)
Expand Down Expand Up @@ -217,7 +217,7 @@ Write clear, concise commit messages that explain **why** the change was made. U

- **Single-service deployment**: In production, NestJS serves both the API and the frontend static files from the same port via `@nestjs/serve-static`.
- **Dev mode**: Vite on `:3000` proxies `/api` and `/v1` to the backend on `:3001`. CORS is enabled only in development.
- **Database**: PostgreSQL 16 for both local development and production. Schema changes are managed via TypeORM migrations (`migrationsRun: true` on boot). After modifying an entity, generate a migration with `npm run migration:generate -- src/database/migrations/Name`.
- **Database**: PostgreSQL 18 for both local development and production. Schema changes are managed via TypeORM migrations (`migrationsRun: true` on boot). After modifying an entity, generate a migration with `npm run migration:generate -- src/database/migrations/Name`.
- **Validation**: Global `ValidationPipe` with `whitelist: true` and `forbidNonWhitelisted: true`.
- **TypeScript**: Strict mode across all packages.

Expand Down
8 changes: 2 additions & 6 deletions deploy/apple-containers/TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ THINKER_PG_VOLUME=my-thinker-data ./deploy/apple-containers/start.sh up
THINKER_RECORDINGS_VOLUME=my-thinker-recordings ./deploy/apple-containers/start.sh up
```

The script mounts the named volume at `/var/lib/postgresql/data` and uses `/var/lib/postgresql/data/pgdata` as `PGDATA`. The nested directory is required because a newly formatted Apple Containers volume may contain a root-level `lost+found` directory.
The script mounts the named volume at `/var/lib/postgresql`. PostgreSQL 18 images keep their data in the version-specific `PGDATA` `/var/lib/postgresql/18/docker`, a nested directory, so a root-level `lost+found` directory on a newly formatted Apple Containers volume does not get in the way. A volume created by an earlier version of this script (PostgreSQL 16, data under `pgdata/`) is not migrated: start with a new volume.

`down` never deletes either named volume. Use Apple Containers volume commands to inspect or deliberately remove them:

Expand Down Expand Up @@ -226,11 +226,7 @@ container logs thinker-app

### PostgreSQL stops during initialization

If logs mention `lost+found`, confirm the script sets:

```text
PGDATA=/var/lib/postgresql/data/pgdata
```
If logs mention `lost+found`, confirm the volume is mounted at `/var/lib/postgresql` (not at the data directory itself) and that `PGDATA` is not overridden, so PostgreSQL uses its default `/var/lib/postgresql/18/docker`.

If logs mention `chmod` or `chown` being denied, do not replace the named volume with a macOS bind mount. The official PostgreSQL image needs filesystem ownership changes that Apple Containers bind mounts do not provide.

Expand Down
7 changes: 3 additions & 4 deletions deploy/apple-containers/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Run the Thinker docker-compose stack with Apple Containers (`container` CLI).
#
# Mirrors docker/docker-compose.yml as closely as Apple Containers allows:
# - postgres:16-alpine with a persistent data directory
# - postgres:18-alpine with a persistent data directory
# - thinker app wired to postgres, with health-wait before starting
# - host LLM access (Ollama / LM Studio) via the container gateway IP
# (Apple Containers has no host.docker.internal)
Expand Down Expand Up @@ -88,7 +88,7 @@ RECORDINGS_VOLUME="${THINKER_RECORDINGS_VOLUME:-thinker-request-recordings}"
# copy of this script run outside the repository.
POSTGRES_IMAGE="$(grep -m1 -oE 'postgres:[^[:space:]"]+@sha256:[0-9a-f]{64}' \
"$SCRIPT_DIR/../../docker/docker-compose.yml" 2>/dev/null || true)"
POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea}"
POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgres:18-alpine@sha256:10a5c69a83035ab5239f5620d708520e8b28f5aa1f2b20143e6024532edadfb5}"

if [[ "$POSTGRES_PASSWORD" != "thinker" && -z "${DATABASE_URL:-}" ]]; then
echo "error: DATABASE_URL must be set when POSTGRES_PASSWORD is customized." >&2
Expand Down Expand Up @@ -232,8 +232,7 @@ cmd_up() {
--env POSTGRES_USER=thinker \
--env POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
--env POSTGRES_DB=thinker \
--env PGDATA=/var/lib/postgresql/data/pgdata \
--volume "$PG_VOLUME:/var/lib/postgresql/data" \
--volume "$PG_VOLUME:/var/lib/postgresql" \
"$POSTGRES_IMAGE"
fi

Expand Down
2 changes: 1 addition & 1 deletion deploy/coolify/TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ curl -sSf https://<your-coolify-domain>/api/v1/health
## What Gets Provisioned

- Thinker Docker image `tmsoftbrasil/thinker:0.1`.
- PostgreSQL 16 container.
- PostgreSQL 18 container.
- Persistent Docker volume for PostgreSQL data.
- Persistent Docker volume for compressed request recordings at `/data/request-recordings`.
- Generated session, encryption, database password, and public URL values.
Expand Down
4 changes: 2 additions & 2 deletions deploy/coolify/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ services:
retries: 3

postgres:
image: postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea
image: postgres:18-alpine@sha256:10a5c69a83035ab5239f5620d708520e8b28f5aa1f2b20143e6024532edadfb5
environment:
- POSTGRES_USER=thinker
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
- POSTGRES_DB=thinker
volumes:
- thinker-postgres-data:/var/lib/postgresql/data
- thinker-postgres-data:/var/lib/postgresql
healthcheck:
test: pg_isready -U thinker
interval: 5s
Expand Down
2 changes: 1 addition & 1 deletion deploy/easypanel/template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ templates/thinker/meta.yaml
The template provisions:

- Thinker app service from `tmsoftbrasil/thinker:0.1`.
- PostgreSQL 16 service.
- PostgreSQL 18 service.
- Persistent app volume for compressed request recordings.
- Generated database password, Better Auth secret, and Thinker encryption key.
- HTTPS domain proxy on port `2099`.
2 changes: 1 addition & 1 deletion deploy/easypanel/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function generate(input: Input): Output {
type: "postgres",
data: {
serviceName: input.databaseServiceName,
image: "postgres:16",
image: "postgres:18",
password: databasePassword,
},
});
Expand Down
2 changes: 2 additions & 0 deletions docker/DOCKER_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ docker compose exec -T postgres pg_dump -U thinker thinker > thinker-backup-$(da

`docker compose down -v` apaga tudo.

O PostgreSQL é o 18: o volume `thinker_pgdata` é montado em `/var/lib/postgresql` e os dados ficam em `/var/lib/postgresql/18/docker`. Não há migração de um volume PostgreSQL 16 (não existem instalações anteriores); com um volume antigo o PostgreSQL 18 recusa subir. Nesse caso, faça dump, `down -v`, suba de novo e restaure.

## Acesso pela rede e proxy reverso

O compose publica a porta só em `127.0.0.1`. Para a LAN, ponha `HOST_BIND_ADDRESS=0.0.0.0` e `BETTER_AUTH_URL` com a URL exata do navegador no `.env`. Atrás de proxy reverso, defina `TRUST_PROXY` e uma `BETTER_AUTH_URL` HTTPS. `BETTER_AUTH_URL` precisa ser alcançável de dentro do container (o MCP busca o JWKS nela). Detalhes: [instalação com Docker](https://github.com/patrickbrandao/thinker/blob/main/docs/usuario/instalacao-docker.md).
Expand Down
7 changes: 5 additions & 2 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,16 @@ services:
- frontend

postgres:
image: postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea
image: postgres:18-alpine@sha256:10a5c69a83035ab5239f5620d708520e8b28f5aa1f2b20143e6024532edadfb5
environment:
- POSTGRES_USER=thinker
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-thinker}
- POSTGRES_DB=thinker
volumes:
- pgdata:/var/lib/postgresql/data
# PostgreSQL 18+ images keep PGDATA in /var/lib/postgresql/18/docker and
# declare the volume at /var/lib/postgresql, so mount the parent. A PG 16
# volume (old /var/lib/postgresql/data layout) is not migrated.
- pgdata:/var/lib/postgresql
healthcheck:
test: pg_isready -U thinker
interval: 5s
Expand Down
6 changes: 3 additions & 3 deletions docs/dev/guia.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Monorepo com npm workspaces e Turborepo:
```text
packages/
├── shared/ thinker-shared tipos e helpers usados por backend, frontend e CLI (provedores, tiers, snippets)
├── backend/ thinker-backend NestJS 11, TypeORM 0.3, PostgreSQL 16, Better Auth
├── backend/ thinker-backend NestJS 11, TypeORM 0.3, PostgreSQL 18, Better Auth
├── frontend/ thinker-frontend SolidJS + Vite, dashboard
├── cli/ thinker-cli CLI `thinker` (privada, não publicada)
└── thinker/ thinker pacote sem código que guarda a versão canônica
Expand Down Expand Up @@ -103,7 +103,7 @@ SEED_DATA=true
| Backend e2e (Docker) | `npm run test:e2e:docker --workspace=packages/backend` |
| Contratos do gateway | `THINKER_BASE_URL=http://127.0.0.1:2099 node --test contracts/gateway/tests/*.test.mjs` |

**E2E só por `test:e2e:docker`.** A suíte usa `dropSchema: true` (apaga o banco). O script `packages/backend/scripts/test-e2e-docker.mjs` escolhe uma porta livre em `127.0.0.1`, sobe um `postgres:16-alpine` com nome único e dados em tmpfs (banco `thinker_e2e_test`), roda as migrações, executa a suíte em série e remove o contêiner no fim (sucesso, falha ou Ctrl-C). Argumentos extras vão para o Jest:
**E2E só por `test:e2e:docker`.** A suíte usa `dropSchema: true` (apaga o banco). O script `packages/backend/scripts/test-e2e-docker.mjs` escolhe uma porta livre em `127.0.0.1`, sobe um `postgres:18-alpine` com nome único e dados em tmpfs montado em `/var/lib/postgresql` (banco `thinker_e2e_test`), roda as migrações, executa a suíte em série e remove o contêiner no fim (sucesso, falha ou Ctrl-C). A imagem é a mesma, com o mesmo digest, de `docker/docker-compose.yml` e do job `Backend e2e (PostgreSQL)` da CI (`THINKER_E2E_PG_IMAGE` troca a imagem). Ao atualizar o digest, atualize os três lugares e `deploy/coolify/docker-compose.yml`. Argumentos extras vão para o Jest:

```bash
npm run test:e2e:docker --workspace=packages/backend -- test/limits.e2e-spec.ts
Expand Down Expand Up @@ -237,7 +237,7 @@ Workflows em `.github/workflows/` (ações fixadas por SHA, token somente leitur

| Workflow | Gatilho | Conteúdo |
| --- | --- | --- |
| `ci.yml` | PRs e pushes na `main` | Lint, typecheck, build, testes unitários por pacote (o do backend também com `TZ=America/Sao_Paulo`), e2e com PostgreSQL 16 (`TZ=UTC` e `America/Sao_Paulo`), scripts |
| `ci.yml` | PRs e pushes na `main` | Lint, typecheck, build, testes unitários por pacote (o do backend também com `TZ=America/Sao_Paulo`), e2e com PostgreSQL 18 (`TZ=UTC` e `America/Sao_Paulo`), scripts |
| `docker.yml` | PRs que tocam a imagem | Build `linux/amd64` e `linux/arm64`, sem publicar |
| `docker-smoke.yml` | PRs que tocam imagem ou compose | Teste de fumaça do Compose (somente leitura) e `install.sh` ponta a ponta |
| `codeql.yml` | PRs, pushes, semanal | Análise de código |
Expand Down
8 changes: 7 additions & 1 deletion docs/usuario/instalacao-docker.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Instalação com Docker

A instalação oficial do Thinker é Docker/Compose: a imagem `tmsoftbrasil/thinker` (`linux/amd64` e `linux/arm64`) e um PostgreSQL 16. Não há instalação via npm.
A instalação oficial do Thinker é Docker/Compose: a imagem `tmsoftbrasil/thinker` (`linux/amd64` e `linux/arm64`) e um PostgreSQL 18. Não há instalação via npm.

## Requisitos

Expand Down Expand Up @@ -140,6 +140,12 @@ docker compose up -d

`docker compose down` mantém os dados; `docker compose down -v` **apaga tudo**.

## PostgreSQL 18

A stack usa o PostgreSQL 18 (`postgres:18-alpine`, fixado por digest). A partir do 18, a imagem oficial guarda os dados em `/var/lib/postgresql/18/docker` e o volume `thinker_pgdata` é montado em `/var/lib/postgresql` (antes era `/var/lib/postgresql/data`). Os volumes novos já nascem com checksums de dados ligados.

**Não há migração de dados de um volume PostgreSQL 16**: o Thinker não tem instalações anteriores com o 16. Se um volume antigo for montado, o contêiner do PostgreSQL 18 recusa subir (mensagem `there appears to be PostgreSQL data in: /var/lib/postgresql`) em vez de apagar ou ignorar os dados. Para quem testou uma versão anterior: faça o dump com a stack antiga, apague o volume (`docker compose down -v`), suba a stack nova e restaure o dump (veja [Backup](#backup)).

## Quem vem do Manifest

O 0.1 só suporta instalação nova. O projeto Compose, o usuário e o banco do PostgreSQL agora se chamam `thinker`, então os volumes são novos e os dados antigos não aparecem. Detalhes em [Notas de release 0.1.0](../releases/0.1.0.md).
Expand Down
10 changes: 7 additions & 3 deletions packages/backend/scripts/test-e2e-docker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* npm run test:e2e:docker --workspace=packages/backend [-- <jest args>]
*
* 1. Picks a free TCP port on 127.0.0.1 (never a fixed one like 5432).
* 2. Starts a uniquely named postgres:16-alpine container, data on tmpfs,
* 2. Starts a uniquely named postgres:18-alpine container, data on tmpfs,
* database `thinker_e2e_test`, published only on 127.0.0.1:<port>.
* 3. Waits until it accepts connections, runs the migrations, then runs the
* e2e suite in band with DATABASE_URL and THINKER_E2E_ALLOW_DROP=1 set.
Expand All @@ -20,7 +20,10 @@ import { createServer } from 'node:net';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const IMAGE = process.env.THINKER_E2E_PG_IMAGE || 'postgres:16-alpine';
// Same digest-pinned image as docker/docker-compose.yml and the CI e2e job.
const IMAGE =
process.env.THINKER_E2E_PG_IMAGE ||
'postgres:18-alpine@sha256:10a5c69a83035ab5239f5620d708520e8b28f5aa1f2b20143e6024532edadfb5';
const DB_NAME = 'thinker_e2e_test';
const DB_USER = 'thinker_e2e';
const DB_PASSWORD = randomBytes(12).toString('hex');
Expand Down Expand Up @@ -142,7 +145,8 @@ async function startContainer() {
'--label',
RUN_LABEL,
'--tmpfs',
'/var/lib/postgresql/data:rw',
// PostgreSQL 18+ keeps PGDATA under /var/lib/postgresql/<major>/docker.
'/var/lib/postgresql:rw',
'--publish',
`127.0.0.1:${port}:5432`,
'--env',
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/test/e2e-db-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export function e2eGuardMessage(reason: string): string {
'',
' npm run test:e2e:docker --workspace=packages/backend',
'',
'That starts a throwaway postgres:16-alpine container on a free port, runs the',
'That starts a throwaway postgres:18-alpine container on a free port, runs the',
'migrations and the suite, and removes the container afterwards.',
'',
'To use a database you manage yourself, it must be dedicated to tests:',
Expand Down
2 changes: 1 addition & 1 deletion scripts/db-reindex-bloated.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
#
# Usage:
# DATABASE_URL=postgresql://...direct, not PgBouncer... scripts/db-reindex-bloated.sh [index ...]
# PSQL="docker run --rm -i --network host postgres:17 psql" DATABASE_URL=... scripts/db-reindex-bloated.sh
# PSQL="docker run --rm -i --network host postgres:18 psql" DATABASE_URL=... scripts/db-reindex-bloated.sh
#
# With no arguments it rebuilds the default list below, smallest first. It
# stops at the first failure after dropping the "<name>_ccnew" leftover, and
Expand Down
Loading