vek1 — decisões técnicas
Decisões técnicas notáveis
Arquitetura: api-first (PR #65–#70)
Decisão central: vek1 (Next 16) virou camada thin de UI + auth orchestration. Toda escrita em DB passa pelo vek1-api via HTTP interno. Drizzle schema permanece source-of-truth, mas mutations e maioria dos reads vão por apiClient.
Razões:
- Single point pra auditoria (audit_log no vek1-api), rate limiting, retry, idempotência
- Ownership check centralizado (
assert_owns_store/order/lead/product) — bug aí está em 1 lugar - vek1-api já é dono dos modelos (chat, embeddings, file processing); spread Drizzle direto duplicava lógica
- Permite eventualmente outras frontends (mobile, ops dashboard) sem reescrever access layer
Tradeoffs:
- 2 hops (Vercel → VPS → Postgres) em cada mutation vs 1 hop direto
- Latência adicional ~30-80ms por call (TLS + network)
- Acoplamento: vek1-api down = vek1 read-only
Mitigação: Cache Components mantém UI rápida pra reads; mutations toleram latência extra.
Token scope segregation
3 tokens isolados (INTERNAL_API_TOKEN, INTERNAL_AUTH_TOKEN, INTERNAL_WEBHOOK_TOKEN). Razão: comprometimento de um escopo (ex: token leakado num client log) não dá acesso aos outros (ex: app token não cria sessions). Fallback pro app token cobre transição.
Cache Components (Next 16)
cacheComponents: trueemnext.config.ts- API routes com cookies/auth fazem
await connection()no topo (force dynamic) - Pages auth-gated: refactor pra cookies em Suspense child, não no topo
lib/queries/*removeu wrappers Reactcache()(incompatível com cacheComponents prerender)revalidatePathem vez derevalidateTag(sig mudou)
Drizzle sem migrations versionadas
bun run db:push --forcedireto contra Postgres prod- Risco: drops indexes não-Drizzle (hnsw vector, unique custom)
- Mitigação:
scripts/recreate-indexes.ts+init/01-init.sqlre-aplicados após push - Tem agent dedicado:
schema-migrator(cuidados especiais com hnsw)
Não usa drizzle-kit generate/migrate: pareceu overhead pra um schema com 1 owner. Pode mudar se schema crescer e equipe expandir.
Better Auth com HTTP adapter custom
createAdapterFactoryemsrc/lib/auth-http-adapter.ts- Per-model dispatcher (não CRUD genérico)
- Razão: keys camelCase vs snake_case + parse timestamps + endpoints purpose-built pro vek1-api
consumeOne(verification)resolve race PKCE: DELETE..RETURNING atomic no Postgres (vs find + delete que tem janela de race)
Storage: single bucket + key prefix
- Antes (Supabase): 1 bucket por company (
company-{slug}-files) - Agora (MinIO): bucket único
vek1, key prefixcompanies/{user.id}/... - Razão: simpler, sem permissões bucket-create, anonymous download policy global, public URL via CDN
TypeScript / Lint / Format
- TS strict
- ESLint estrito:
no-explicit-anyerror, import ordering comnewlines-between - Prettier: singleQuote, trailingComma es5, arrowParens=avoid, printWidth=80
- Path alias:
@/*→src/*
Testes
- Vitest 4 + Testing Library + jsdom (migração Jest → Vitest concluída em PR #40)
vitest.config.ts+ setup file- Tests reais:
orders-*.test.ts(5 arquivos, e2e + state machine + calc + stock),abacate-pay.test.ts,stock-sync-actions.test.ts,agents/documents/products/utils/redirect/badge/store-actions/check-duplicates - UI components mostly uncovered (gap conhecido)
CI/CD
3 jobs (Bun frozen-lockfile): lint → test → build. Envs CI = placeholders (PR #44, sem Supabase).
Server Actions
- Todas thin wrappers sobre
apiClient— auth/ownership check + chamada revalidatePathaplicado consistentemente- Validação Zod parcial (alguns actions); maioria delega validação pro Pydantic strict no vek1-api
Padrão SSR Cache Components
Pages tipo /leads/page.tsx:
export default async function LeadsPage() {
const user = await requireUser();
return (
<Suspense fallback={<LeadsListSkeleton />}>
<LeadsList userId={user.id} />
</Suspense>
);
}
async function LeadsList({ userId }: { userId: string }) {
'use cache';
cacheTag(`leads-${userId}`);
const data = await apiClient.leads.list({ ... });
return <LeadsTable data={data} />;
}
Mas auth-gated pages que dependem de cookies dinâmicos usam await connection() em vez de 'use cache'.
Convenções de pastas
- Server Components default;
'use client'só interativo - Mutações só via
apiClient - Storage centralizado em
lib/storage.ts - Sem
any; interfaces explícitas - Estilo só Tailwind +
cn() - Componentes em
components/[feature]/(alguns arquivos soltos emcomponents/raiz — gotcha histórico)
Scripts
bun run db:push— drizzle-kit push schemabun run db:studio— drizzle studiobun run test— vitest- Sem
generate:types(não usa mais Supabase CLI)