pacforge
v0.2.1
Published
Orquestrador e painel self-hosted para múltiplas instâncias do PocketBase.
Maintainers
Readme
⚒️ PacForge
PacForge é um orquestrador e painel de gerenciamento self-hosted para múltiplas instâncias do PocketBase. Projetado para ser leve, seguro e com uma UX excepcional, permite que desenvolvedores gerenciem bancos de dados BaaS com o mesmo conforto de plataformas SaaS, mas rodando em sua própria infraestrutura.
✨ Destaques
- Node.js nativo — sem React, sem Vue, sem build steps complexos. Apenas
http,child_process,fsebetter-sqlite3. - Multi-instância — cada projeto PocketBase roda como um processo isolado em
127.0.0.1:<porta_aleatória>, nunca exposto diretamente à internet. - Reverse proxy transparente — roteamento por subdomínio (
app1.seudominio.com) ou por path (/i/app1/) para dev local sem DNS wildcard. - Download automático do PocketBase — na primeira execução, o binário é baixado automaticamente do GitHub releases, com detecção de OS/arquitetura.
- Auto-restart com backoff exponencial — se uma instância crashear, ela é reiniciada automaticamente (1s, 2s, 4s, 8s, 16s), até 5 tentativas.
- Painel SaaS-like — dark mode com glassmorphism, terminal embutido (xterm.js) com logs em tempo real via WebSocket, toasts, modais e micro-interações.
- Seguro por padrão — bcrypt para senhas, JWT com expiração curta (15 min), rate limiting no login (5 tentativas / 15 min), regex estrita para nomes de instância.
- CLI completo —
pacforge start,instances:create,admin:create, etc. Para administração via SSH/CI sem expor o painel.
📑 Sumário
- Início Rápido
- Arquitetura
- CLI Reference
- API Reference
- Configuração
- Deploy
- Desenvolvimento
- Testing & Quality Assurance
- Troubleshooting & FAQ
- Contributing
- License
🚀 Início Rápido
Pré-requisitos
- Node.js >= 18 (recomendado 20+)
- unzip instalado no sistema (para extrair o binário PocketBase no primeiro run)
- Linux, macOS ou Windows
Instalação e primeiro run
# 1. Clone o repo
git clone https://github.com/pacdt/pacforge.git
cd pacforge
# 2. Instale dependências
npm install
# 3. Build do frontend (Tailwind CSS + cópia dos JS)
npm run build
# 4. Crie um usuário admin para o painel
node bin/cli.js admin:create --email [email protected] --password S3nh4F0rt3
# 5. Inicie o Master Process em background
node bin/cli.js start --daemon
# 6. Abra o painel no navegador
# http://localhost:4001Criando sua primeira instância
# Via CLI
node bin/cli.js instances:create meu-app
# → Instance created: meu-app (port 38211)
# URL: http://meu-app.localhost:4001
# Admin UI: http://meu-app.localhost:4001/_/
# Ou via painel web: botão "New Instance" no canto superior direitoAcesse http://meu-app.localhost:4001/_/ para configurar o superusuário do PocketBase dessa instância (ou use o botão "Reset Superuser" no card do painel).
🏗️ Arquitetura
O PacForge opera como um Master Process em Node.js que gerencia múltiplos Child Processes do binário PocketBase.
flowchart TD
subgraph Master ["PacForge Master (Node.js)"]
R["Router<br>(Proxy)"]
PM["Process<br>Manager"]
AP["Admin Panel<br>(HTTP + WS)"]
HS["HTTP Server<br>(Port 4001)"]
SS["State Store<br>(SQLite)"]
TUI["Tailwind UI<br>(Vanilla JS)"]
R ==> HS
PM ==> SS
AP ==> TUI
end
PB1[/"PocketBase Proc<br/>(Inst: app1)<br/>127.0.0.1:32111"\]
PB2[/"PocketBase Proc<br/>(Inst: app2)<br/>127.0.0.1:32122"\]
HS -.-> PB1
SS -.-> PB2
classDef default fill:#1e293b,stroke:#475569,stroke-width:2px,color:#f8fafc,rx:8px,ry:8px;
classDef master fill:#0f172a,stroke:#334155,stroke-width:2px,color:#e2e8f0;
classDef proc fill:#0369a1,stroke:#0284c7,stroke-width:2px,color:#ffffff,rx:12px,ry:12px;
class Master master;
class PB1,PB2 proc;Como funciona o roteamento
O Master Process escuta em PACFORGE_PORT (default 4001). Ele analisa o Host header:
| Host | Path | Ação |
| ------------------------ | ------------- | ----------------------------------------------- |
| localhost:4001 | / | Serve o painel HTML |
| localhost:4001 | /api/* | API REST interna |
| localhost:4001 | /auth/* | Rotas de autenticação |
| localhost:4001 | /ws/logs/* | WebSocket para terminal de logs |
| localhost:4001 | /i/<name>/* | Path-based fallback: proxy para a instância |
| meu-app.localhost:4001 | /* | Subdomain: proxy para a instância meu-app |
| meu-app.seudominio.com | /* | Subdomain: proxy para a instância meu-app |
Segurança: as instâncias do PocketBase NUNCA escutam em 0.0.0.0. Elas rodam estritamente em 127.0.0.1:<porta_aleatoria>. O único exposto à internet é o Master Process.
Estrutura de arquivos
pacforge/
├── bin/
│ └── cli.js # Entry point CLI (start, stop, admin:create, ...)
├── src/
│ ├── master.js # Servidor HTTP + WebSocket + API REST
│ ├── processManager.js # Spawn/kill/restart do PocketBase + auto-restart
│ ├── router.js # Proxy reverso por subdomínio + path-based
│ ├── database.js # Camada SQLite (State Store)
│ ├── security.js # bcrypt + JWT + regex + rate limiter
│ └── logger.js # Logger com níveis (debug/info/warn/error)
├── frontend/
│ ├── src/
│ │ ├── main.js # Lógica principal do painel (login, dashboard)
│ │ ├── components.js # Builders de DOM (cards, modais, toasts)
│ │ └── styles.css # Tailwind directives + custom CSS
│ └── dist/ # CSS compilado (gerado pelo build)
├── public/
│ ├── index.html # HTML do painel
│ └── assets/ # CSS + JS copiados pelo build (servidos estaticamente)
├── test/
│ ├── security.test.js # Unit tests: bcrypt, JWT, regex, rate limiter
│ ├── database.test.js # Unit tests: SQLite schema e CRUD
│ ├── router.test.js # Unit tests: host parsing, classify, path matching
│ ├── processManager.test.js # Integration: download PB, provision/destroy
│ └── master.test.js # Integration: HTTP server, API, proxy reverso
├── scripts/
│ └── build-frontend.js # Copia frontend/src/*.js → public/assets/
├── deploy/
│ └── pacforge.service # systemd unit file
├── Dockerfile # Multi-stage build
├── .eslintrc.json # ESLint config (standard)
├── .prettierrc.json # Prettier config
├── tailwind.config.js # Tailwind config (cores, fontes, animações)
└── package.jsonState Store (SQLite)
O PacForge mantém um banco SQLite em ~/.pacdt/pacforge.db com 3 tabelas:
users— usuários autorizados a acessar o painel (email, password_hash bcrypt, role)instances— metadados das instâncias PocketBase (id, name, port, status, pid)settings— config global (jwt_secret, etc.)
Os dados das instâncias PocketBase em si (collections, records, auth) ficam em ~/.pacforge/data/<name>/pb_data/ — o PacForge nunca toca nesse diretório exceto para deletá-lo quando você pede para remover a instância.
🖥️ CLI Reference
pacforge <command> [options]Servidor
| Comando | Descrição |
| ---------------- | ---------------------------------------------------- |
| start | Inicia o Master Process em foreground |
| start --daemon | Inicia como daemon em background (escreve PID file) |
| stop | Para um daemon em execução (SIGTERM, depois SIGKILL) |
| status | Mostra status do daemon (PID, porta, apex) |
Usuários do painel
| Comando | Descrição |
| ------------------------------------- | ------------------------ |
| admin:create --email E --password P | Cria um usuário admin |
| admin:list | Lista usuários do painel |
| admin:delete --email E | Remove um usuário |
Instâncias
| Comando | Descrição |
| ----------------------------------- | ----------------------------------------------------- |
| instances:list | Lista todas as instâncias (name, status, port, pid) |
| instances:create <name> | Cria e inicia uma nova instância |
| instances:delete <name> [--force] | Para e deleta permanentemente a instância + dados |
| instances:start <name> | Inicia uma instância parada |
| instances:stop <name> | Para uma instância em execução |
| instances:restart <name> | Reinicia uma instância |
Binário PocketBase
| Comando | Descrição |
| ------------- | --------------------------------------- |
| pb:download | Força re-download do binário PocketBase |
Exemplos
# Criar usuário admin
pacforge admin:create --email [email protected] --password S3nh4F0rt3
# Iniciar como daemon
pacforge start --daemon
# Criar instância via CLI (útil para CI/SSH)
pacforge instances:create blog-prod
# → Instance created: blog-prod (port 38211)
# URL: http://blog-prod.localhost:4001
# Listar
pacforge instances:list
# → Instances:
# blog-prod running port=38211 pid=12345 http://blog-prod.localhost:4001
# Deletar (com confirmação)
pacforge instances:delete blog-prod --force🔌 API Reference
Todas as rotas (exceto /auth/login) exigem header Authorization: Bearer <JWT>.
Autenticação
POST /auth/login
Autentica e retorna um JWT (válido por 15 minutos).
POST /auth/login
Content-Type: application/json
{
"email": "[email protected]",
"password": "securepassword"
}{
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 900,
"user": { "email": "[email protected]", "role": "admin" }
}Rate limited: 5 tentativas por IP a cada 15 minutos.
GET /auth/me
Retorna o usuário autenticado pelo token atual.
Instâncias
GET /api/instances
Lista todas as instâncias com status em tempo real, uso de disco e URL pública.
[
{
"id": "uuid-1234",
"name": "my-app",
"status": "running",
"port": 32111,
"pid": 12345,
"size": "12.3M",
"url": "http://my-app.localhost:4001",
"createdAt": "2026-08-05 10:00:00",
"updatedAt": "2026-08-05 10:30:00"
}
]POST /api/instances
Cria e provisiona uma nova instância. O nome deve passar pela regex ^[a-z0-9](?!.*--)[a-z0-9-]*[a-z0-9]$ (2–32 chars, sem hifens duplos).
POST /api/instances
Authorization: Bearer <token>
Content-Type: application/json
{ "name": "new-project" }{
"id": "uuid-5678",
"name": "new-project",
"status": "running",
"port": 32122
}DELETE /api/instances/:name
Para o processo, apaga a pasta pb_data e remove o registro do banco. Irreversível.
DELETE /api/instances/new-project
Authorization: Bearer <token>→ 204 No Content
Gerenciamento de Processos
| Endpoint | Método | Descrição |
| ---------------------------------- | ------ | -------------------------------------- |
| /api/instances/:name/start | POST | Inicia o processo (se parado) |
| /api/instances/:name/stop | POST | Envia SIGTERM (depois SIGKILL após 5s) |
| /api/instances/:name/restart | POST | Stop + start |
| /api/instances/:name/logs?tail=N | GET | Histórico de logs (últimas N linhas) |
Administração do PocketBase
POST /api/instances/:name/admin
Cria ou atualiza o superusuário do PocketBase via CLI do binário (pocketbase superuser upsert).
POST /api/instances/my-app/admin
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "[email protected]",
"password": "anothersecurepassword"
}{ "ok": true, "message": "Superuser upserted successfully." }WebSocket: Terminal de logs
WS /ws/logs/<instance-name>?token=<JWT>Streaming ao vivo de stdout + stderr do PocketBase. Histórico das últimas 1000 linhas é replayado ao conectar. Usado pelo terminal embutido (xterm.js) no painel.
⚙️ Configuração
Todas as configurações são via variáveis de ambiente. Veja .env.example para o template completo.
| Variável | Descrição | Padrão |
| -------------------------- | ------------------------------------------------------- | ------------- |
| PACFORGE_PORT | Porta do Master Process (painel + proxy) | 4001 |
| PACFORGE_HOST | Host/interface para bind (0.0.0.0 para expor) | 0.0.0.0 |
| APEX_DOMAIN | Domínio base para roteamento de subdomínios | localhost |
| PACFORGE_HOME | Diretório base de dados (SQLite + binário + instâncias) | ~/.pacforge |
| PB_VERSION | Versão do binário PocketBase | 0.22.14 |
| JWT_SECRET | Chave JWT (auto-gerada se vazio, persistida no SQLite) | (auto) |
| LOG_LEVEL | debug | info | warn | error | info |
| ENABLE_HTTPS | Habilita HTTPS com Let's Encrypt (em produção) | false |
| PACFORGE_DEV_AUTH_BYPASS | Bypass de auth em dev (NUNCA usar em prod) | false |
Estrutura de diretórios em PACFORGE_HOME
~/.pacforge/
├── pacforge.db # State Store SQLite
├── pacforge.db-journal # WAL journal (auto)
├── pacforge.pid # PID file (quando rodando como daemon)
├── bin/
│ └── pocketbase # Binário PB baixado automaticamente
└── data/
├── meu-app/ # Dados da instância "meu-app"
│ ├── pb_data/ # Banco SQLite do PocketBase (NUNCA tocar manualmente)
│ └── ... # Outros arquivos do PB
└── outro-app/
└── pb_data/🐳 Deploy
Opção 1: NPM (desenvolvedores)
npm install -g pacforge
pacforge admin:create --email [email protected] --password S3nh4F0rt3
pacforge start --daemonOpção 2: Docker
# Build
docker build -t pacforge .
# Run com volume persistente
docker run -d \
--name pacforge \
-p 4001:4001 \
-v pacforge-data:/data \
-e APEX_DOMAIN=seudominio.com \
-e JWT_SECRET=$(openssl rand -hex 32) \
pacforge
# Criar usuário admin (após container estar rodando)
docker exec -it pacforge node bin/cli.js admin:create --email [email protected] --password S3nh4F0rt3O Dockerfile é multi-stage: build do Tailwind/JS em um estágio, runtime enxuto em outro. Usa tini como init para tratar SIGTERM corretamente.
Opção 3: Systemd (bare metal Linux)
# Instalar globalmente
sudo npm install -g pacforge
# Criar usuário do sistema
sudo useradd -r -s /bin/false -d /var/lib/pacforge pacforge
sudo mkdir -p /var/lib/pacforge
sudo chown pacforge:pacforge /var/lib/pacforge
# Instalar service
sudo cp deploy/pacforge.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now pacforge
# Ver logs
sudo journalctl -u pacforge -fO service file inclui hardening systemd: NoNewPrivileges, ProtectSystem=full, ProtectKernelTunables, etc.
Opção 4: Atrás de Nginx/Caddy (recomendado em produção)
Em produção, recomendamos colocar o PacForge atrás de um reverse proxy (Nginx, Caddy, Traefik) que trata TLS e passa os headers corretamente:
server {
listen 443 ssl http2;
server_name *.seudominio.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:4001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}🛠️ Desenvolvimento
Setup
git clone https://github.com/pacdt/pacforge.git
cd pacforge
npm install
npm run build # build inicial do CSS + JSModo dev com hot reload
# Terminal 1: watch do Tailwind CSS
npm run build:css:dev
# Terminal 2: master com --watch (reinicia em mudanças no src/)
npm run devScripts NPM
| Script | Descrição |
| ------------------- | --------------------------------------------------- |
| npm start | Inicia o master em foreground |
| npm run dev | Inicia com node --watch (hot reload do backend) |
| npm run build | Build completo: Tailwind CSS + cópia dos JS |
| npm run build:css | Apenas Tailwind (compila + minifica) |
| npm run build:js | Apenas copia frontend/src/*.js → public/assets/ |
| npm test | Roda todos os testes (unit + integration) |
| npm run lint | ESLint |
| npm run format | Prettier |
Estrutura do frontend
O frontend é Vanilla JS com ES modules — sem bundler. O scripts/build-frontend.js apenas copia frontend/src/*.js para public/assets/ (renomeando main.js → app.js para casar com o <script src> do HTML).
Bibliotecas externas (xterm.js) são carregadas via CDN no index.html. Se você precisa de offline total, baixe os arquivos do CDN para public/assets/vendor/ e ajuste as tags <script>/<link>.
🧪 Testing & Quality Assurance
O projeto usa o Node.js Native Test Runner (node:test), sem dependências externas de teste.
Rodar os testes
# Todos os testes
npm test
# Apenas unit tests (rápido, sem download de binário)
node --test test/security.test.js test/database.test.js test/router.test.js
# Apenas integration tests (baixa PocketBase real na primeira execução)
node --test test/processManager.test.js test/master.test.js
# Watch mode
npm run test:watchCobertura dos testes
| Arquivo | Tipo | Cobertura |
| ----------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| test/security.test.js | Unit | isValidName, bcrypt hash/verify, JWT issue/verify, extractBearer, RateLimiter, safeEqual |
| test/database.test.js | Unit | Schema init, user CRUD, instance CRUD, settings upsert, JWT secret persistence |
| test/router.test.js | Unit | parseHost (apex, subdomain, localhost, case), matchPathInstance, classify |
| test/processManager.test.js | Integration | getFreePort, detectPlatform, download do binário real, provision + destroy lifecycle, LogBus pub/sub |
| test/master.test.js | Integration | Boot do servidor, login JWT, auth middleware (401 sem token), create/list/stop/delete instance, reverse proxy alcançando PocketBase real, rate limiting |
Resultado atual: 56 testes, 56 passando.
Linting e formatação
- ESLint com config
standard - Prettier com
semi: false, singleQuote: true - Husky + lint-staged rodam ESLint + Prettier nos arquivos staged antes de cada commit
npm run lint # check
npm run lint:fix # auto-fix
npm run format # prettier em tudo🩺 Troubleshooting & FAQ
Q: Criei uma instância, mas ao acessar http://app.localhost:4001/_/ recebo "Bad Gateway".
R: O Master Process não conseguiu se comunicar com o processo interno do PocketBase. Possíveis causas:
- Binário sem permissão de execução — rode
chmod +x ~/.pacforge/bin/pocketbase - Porta interna colidiu e o processo crashou — verifique os logs no terminal embutido do painel
- Download do binário falhou — rode
pacforge pb:downloadpara forçar re-download unzipnão instalado — instale viaapt install unzip(Linux) oubrew install unzip(macOS)
Q: Como faço para atualizar a versão do PocketBase de uma instância?
R:
- Pare o PacForge:
pacforge stop - Altere
PB_VERSIONno ambiente (ex:PB_VERSION=0.23.0) - Force re-download:
pacforge pb:download - Inicie o PacForge:
pacforge start --daemon - Reinicie cada instância pelo painel (ou
pacforge instances:restart <name>)
O PocketBase lida com migrações automáticas de banco ao iniciar com um binário mais novo.
Q: Posso usar o PacForge atrás de um Nginx ou Caddy?
R: Sim, e é recomendado em produção. Veja a seção Deploy → Opção 4. O PacForge usa o header Host para rotear para a instância correta, então o Nginx precisa passar proxy_set_header Host $host;.
Q: Os dados das minhas instâncias somem quando reinicio o servidor.
R: Verifique:
- Docker sem volume: mapeie
-v pacforge-data:/data(ou oPACFORGE_HOMEque você configurou) - systemd: confirme que
ReadWritePaths=/var/lib/pacforgeestá no service file - Pasta
~/.pacforge/datasendo limpa: algum serviço de limpeza do sistema (tmpwatch, etc.) pode estar removendo. Adicione a pasta à lista de exclusões.
O PacForge nunca deleta pb_data a menos que você explicitamente clique em "Delete" no painel ou rode instances:delete.
Q: Esqueci a senha do admin do painel.
R: Crie um novo admin via CLI:
pacforge admin:create --email [email protected] --password NovaSenha123
# Ou delete o antigo:
pacforge admin:delete --email [email protected]Q: Como rodar em desenvolvimento sem auth?
R: Defina PACFORGE_DEV_AUTH_BYPASS=true e NODE_ENV=development. NUNCA use isso em produção.
Q: O WebSocket do terminal não conecta.
R: Verifique:
- O token JWT não expirou (15 min). Faça login novamente.
- Se atrás de Nginx, confirme
proxy_http_version 1.1e headersUpgrade/Connection(veja exemplo acima). - Browser console deve mostrar erro específico.
Q: Como mudar a porta de uma instância existente?
R: Não é suportado diretamente. As portas são alocadas automaticamente. Para "resetar":
- Pare a instância:
pacforge instances:stop <name> - Delete e recrie:
pacforge instances:delete <name> --force && pacforge instances:create <name>
(os dados serão perdidos — faça backup antes se necessário)
🤝 Contributing
Contribuições são bem-vindas! Veja CONTRIBUTING.md para detalhes. Em resumo:
- Fork + branch:
git checkout -b feat/nome-da-feature - Siga Conventional Commits:
feat:,fix:,docs:,refactor:,test:,chore: - Mantenha o footprint mínimo de dependências (regra 14.2 da spec)
- Todo
async/awaitdeve tertry/catch - Use o logger interno, não
console.logdireto - Adicione testes para novas features
- Abra um PR detalhando o quê e o porquê
📄 License
MIT © PacForge Contributors
