ShesBlej
코소보에서 매매·임대 아파트·차량·중고 물품 광고, 무료로 올리고 판매자와 직접 연락합니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Plataforma global para venda de produtos digitais, cursos, comunidades, assinaturas e mentorias.
A Kabrun recebe o pagamento do comprador, registra a venda no seu próprio ledger, cobra a taxa da plataforma e repassa o saldo ao produtor quando o prazo de liberação vence. Ou seja: é uma plataforma de pagamentos/marketplace, não uma loja com checkout integrado.
Mercados da Fase 1: Brasil, União Europeia e Estados Unidos (BRL, EUR, USD).
Cada camada tem uma responsabilidade única, e elas nunca se misturam:
| Camada | Responsabilidade |
|---|---|
| Keycloak | Identidade: login, senha, recuperação, verificação de e-mail, sessão, MFA |
| Kabrun | Autorização: quem pertence a qual organização e o que pode fazer |
| PostgreSQL | Fonte de verdade de commerce e finanças |
| MongoDB | Dados flexíveis: apresentação do checkout, eventos, payloads |
| Redis | Cache, locks, rate limit e (futuramente) filas |
| Payment provider | Movimentação real do dinheiro (Stripe) |
| Ledger | Fonte de verdade financeira da Kabrun |
| Balance | Projeção do ledger (cache), nunca fonte de verdade |
| Order | Intenção comercial |
| Payment | Movimentação vinda do comprador |
| Payout | Movimentação indo para o produtor |
O fluxo de uma requisição:
Server Component / Server Action / Route Handler
↓ (Zod valida a entrada, requireUser/requireOrganization autoriza)
Service ← regra de negócio, um domínio por service
↓
Repository ← interface; a implementação Prisma/Mongo fica isolada
↓
PostgreSQL / MongoDB ← operações financeiras sempre em transação
Serviços nunca importam PrismaClient, MongoClient ou o SDK da Stripe. Eles recebem
repositories e interfaces de provider (inversão de dependência), o que também torna os testes
determinísticos. A montagem fica em src/server/services.ts e
src/server/container.ts.
jose)@prisma/adapter-pg)MockPaymentProvider para desenvolvimentoPostgreSQL é a fonte de verdade de tudo que é relacional e financeiro:
User, Organization, OrganizationMember, Product, Offer, Checkout, Customer, Order,
OrderItem, Payment, Transaction, Subscription, Refund, Dispute, LedgerJournal,
LedgerEntry, Balance, Reserve, Payout, PayoutDestination, MerchantAccount,
SettlementPolicy, PlatformFeePolicy, Affiliate, Coupon, Course, Enrollment,
ApiKey, AuditLog, WebhookEvent, OutboxEvent, IdempotencyKey.
MongoDB guarda o que é flexível, volumoso ou muda de forma com frequência:
checkout_configs (versionado), analytics_events, webhook_payloads, provider_snapshots,
activity_events, risk_events.
Regra que não se quebra: dado financeiro nunca tem o MongoDB como única fonte de verdade.
O preço oficial vem sempre da Offer no PostgreSQL; o Mongo só configura a apresentação.
Realm kabrun, client kabrun-web (confidencial, PKCE S256). A Kabrun não armazena senhas:
a tabela User guarda keycloakUserId (único), e-mail, nome e preferências.
GET /api/auth/login inicia o fluxo (state + nonce + PKCE em cookie assinado de 10 min)GET /api/auth/callback valida state/nonce, troca o code, verifica o ID token via JWKS e cria a sessãoPOST /api/auth/logout limpa a sessão e encerra a sessão no Keycloak (RP-initiated logout)A sessão é um JWT HS256 em cookie httpOnly, SameSite=Lax (e Secure em produção).
O src/proxy.ts faz apenas a checagem otimista (existe sessão válida?);
a autorização real acontece no servidor, em cada página e action.
Toda entidade de negócio carrega organizationId, e todo repositório filtra por ele.
Helpers: getCurrentUser(), requireUser(), requireOrganization(), requireRole(),
requirePermission(). O organizationId que chega do cliente (cookie, formulário, URL) nunca é
confiado: o vínculo é sempre reconferido no banco.
Papéis: OWNER, ADMIN, FINANCE, SUPPORT, MARKETING, VIEWER (mapeados para permissões em
permissions.ts), mais o papel global PLATFORM_ADMIN
para a equipe Kabrun (/admin).
A marca é roxa em ~70% da interface; o dourado fica reservado a 10–15% (CTA principal, o raio, selos premium), para a Kabrun parecer uma fintech moderna e não uma marca de joias.
| Token | Valor | Uso |
|---|---|---|
| Roxo principal | #4B168C | marca, botões primários, ícones de destaque |
| Roxo escuro | #220A45 | sidebar, gradiente de fundo, modo escuro |
| Roxo vibrante | #7C3AED | gráficos, estado ativo, foco e links |
| Dourado principal | #D9A52A | CTAs decisivos e selos |
| Dourado claro | #F2C75C | gradiente e hover do dourado |
| Cinza muito claro | #F6F5F8 | fundo do dashboard |
| Cinza médio | #8B8793 | texto secundário |
| Preto azulado | #17121F | texto principal |
Gradientes: .bg-brand-gradient (#220A45 → #7C3AED) e .bg-gold-gradient (#B98012 → #F2C75C),
definidos em globals.css junto com os tokens de sombra (shadow-soft,
shadow-card, shadow-lift) e os utilitários bg-aurora, bg-dot-grid e text-brand-gradient.
Tipografia: Plus Jakarta Sans nos títulos e números (font-heading), Inter no texto de
interface. Valores monetários usam a classe .tabular para alinhar as colunas.
Arte oficial em public/brand: kabrun-icon.png (transparente, serve em qualquer
fundo), kabrun-logo.png (lockup completo) e kabrun-wordmark-light.png (versão knockout usada na
sidebar escura). O favicon sai de src/app/icon.png.
Gráficos seguem uma paleta validada para daltonismo (roxo #7C3AED como série principal, dourado
#B98012 como segunda série), com linha de 2px, grade recessiva, tooltip com crosshair e nenhum
número impresso sobre cada ponto.
Nunca usamos float. Todo valor é inteiro (bigint) em unidades menores + moeda:
money(1090, "BRL") // R$ 10,90
money(2999, "USD") // US$ 29,99
Helpers em src/lib/money: formatMoney, addMoney, subtractMoney,
calculatePercentage (em basis points), allocateProportionally (rateio sem perder centavos),
parseDecimalToMinorUnits, convertMinorUnits. Operar entre moedas diferentes lança
CurrencyMismatchError — conversão precisa ser explícita.
Comprador → Checkout Kabrun → PaymentProvider → PaymentIntent
↓
pagamento confirmado (webhook)
↓
Kabrun Ledger → Saldo pendente do produtor → Settlement → Saldo disponível
↓
Payout → conta bancária do produtor
A regra mais importante: o frontend nunca confirma pagamento. Um Payment só vira PAID
depois de um evento verificado do provedor. O retorno do navegador serve só para mostrar status.
A interface PaymentProvider isola o domínio do SDK:
OrderService → PaymentService → PaymentProvider → StripePaymentProvider
└ MockPaymentProvider (dev/testes)
PAYMENT_PROVIDER=stripe|mock decide qual usar. O mock é determinístico, não move dinheiro e
emite webhooks assinados — o fluxo de confirmação é idêntico ao de produção.
Stripe: StripePaymentProvider + StripeConnectService (contas conectadas, onboarding hospedado).
Cada produtor tem um MerchantAccount (entidade genérica, não "StripeAccount") cujo status interno
(PENDING, REQUIRES_ACTION, UNDER_REVIEW, ACTIVE, RESTRICTED, DISABLED) é derivado de
charges_enabled, payouts_enabled, details_submitted e dos requirements — nunca de um booleano só.
Partida dobrada desde o primeiro dia. Um LedgerJournal agrupa LedgerEntry e só é aceito se
débitos == créditos. Entries são imutáveis: o repositório não expõe update nem delete, e
correções são novos lançamentos.
Contas: PLATFORM_CASH, PRODUCER_PENDING, PRODUCER_AVAILABLE, PLATFORM_REVENUE,
PROCESSOR_FEES, REFUNDS, CHARGEBACKS, RESERVES, TAXES, PAYOUTS.
Venda de R$ 100,00 com R$ 4,00 de taxa de processamento e R$ 10,00 de taxa Kabrun:
| Conta | Direção | Valor |
|---|---|---|
PLATFORM_CASH | DEBIT | 10000 |
PROCESSOR_FEES | CREDIT | 400 |
PLATFORM_REVENUE | CREDIT | 1000 |
PRODUCER_PENDING | CREDIT | 8600 |
Toda movimentação passa pelo LedgerService, com idempotencyKey por evento financeiro
(payment:{id}:captured, payment:{id}:settled, refund:{id}, payout:{id}:paid…). Repetir a
mesma chave é no-op — é isso que impede um webhook duplicado de criar dinheiro.
Balance (pendente / disponível / reservado por organização e moeda) é projeção recalculada a
partir do ledger dentro da mesma transação do lançamento. Nunca fazemos balance += x, e o saldo
não é a soma das Orders (isso ignoraria taxas, reembolsos e chargebacks).
Depois da venda confirmada, o valor do produtor entra como pendente. A liberação segue a
SettlementPolicy (D+2, D+7, D+14, D+30…), resolvida por organização → país → padrão global.
Nada é hardcoded: as políticas são dados (veja prisma/seed.ts).
Ter saldo disponível na Stripe não libera nada: o settlement interno da Kabrun é independente.
npm run settlement:run # libera o que já venceu
npm run settlement:simulate # finge que a janela passou (desenvolvimento)
Produtor → valida MerchantAccount (ACTIVE + payoutsEnabled)
→ valida saldo disponível (lock da linha de Balance + leitura do ledger)
→ valida mínimo por moeda
→ cria Payout + lançamento (AVAILABLE → PAYOUTS)
→ provider (idempotency key payout:{id})
→ webhook → PAID (PAYOUTS → PLATFORM_CASH)
O que a plataforma não permite: saque acima do disponível, saque usando saldo pendente ou reservado, saque duplicado por retry, saque para conta não verificada, e marcar como pago sem confirmação do provedor. Falha ou cancelamento gera lançamento compensatório que devolve o valor ao saldo disponível.
REFUNDS). Nunca é
possível reembolsar mais do que foi pago, nem duas vezes com a mesma chave.RESERVES. Ganhou, volta para o produtor;
perdeu, sai do caixa da plataforma (CHARGEBACKS) e o pagamento vira CHARGEBACK.Endpoint (POST /api/webhooks/stripe, POST /api/webhooks/mock) faz apenas:
provider + providerEventId); 4. enfileira; 5. responde 2xx.O processamento pesado roda no worker, com backoff e retry. Há três camadas de proteção contra
duplicidade: unicidade do evento, claim atômico no processamento e idempotencyKey do journal.
Mudanças de estado importantes gravam um OutboxEvent na mesma transação (transactional
outbox), e o worker publica os eventos de domínio depois — sem o risco de "banco atualizou, evento
não saiu".
Pré-requisitos: Node 22+, Docker (Docker Desktop, OrbStack ou Colima).
cp .env.example .env
Gere os dois segredos locais (SESSION_SECRET e DATA_HASH_SECRET):
openssl rand -base64 48
Suba a infraestrutura (PostgreSQL, MongoDB, Redis, Keycloak + banco próprio, Mailpit):
npm run infra:up
Instale, prepare os bancos e rode:
yarn install && yarn db:deploy && yarn db:seed && yarn mongo:setup && yarn dev
O gerenciador é o Yarn, porque é o que a Vercel usa para construir a produção — o
yarn.locké o lockfile do projeto e o CI instala com--frozen-lockfile. Rodarnpm installaqui reescreve oyarn.lockem formato npm, o que muda silenciosamente a árvore que vai ao ar. Para executar um script isolado,npm run <script>continua funcionando; é só a instalação que precisa ser pelo Yarn.
Confira se tudo está no ar:
npm run check:infra
Aplicação: http://localhost:3100 · Keycloak: http://localhost:8080 (admin/admin) · Caixa de e-mail: http://localhost:8025
O Mailpit recebe o que o Keycloak envia — recuperação de senha e verificação de e-mail —, que é
um caminho separado do EMAIL_PROVIDER da aplicação. Os e-mails da própria Kabrun (compra
confirmada, saque pago) continuam indo para EMAIL_PREVIEW_DIR como arquivo.
Como verifyEmail está ligado, uma conta criada pelo cadastro só entra depois de clicar no link
que chega no Mailpit. Os links valem 12 horas (verificação) e 30 minutos (recuperação de senha) —
o padrão do Keycloak é 5 minutos para os dois, curto demais para quem só abre o e-mail mais tarde.
Os usuários de desenvolvimento do realm já nascem verificados.
Usuários de desenvolvimento do realm: [email protected] e [email protected] (este com o papel
platform_admin), senha kabrun-dev-123.
O worker de background (retry de webhooks, outbox e settlement) roda separado:
npm run worker
MongoDB local roda sem autenticação, preso a
127.0.0.1(omongoshdo container falha em alguns kernels virtualizados, o que quebra o bootstrap do usuário). Em staging/produção o Mongo precisa subir com autenticação.
Todas em .env.example, validadas com Zod no boot (src/lib/env.ts).
Destaques: DATABASE_URL, MONGODB_URI, REDIS_URL, KEYCLOAK_*, APP_URL, SESSION_SECRET,
DATA_HASH_SECRET, PAYMENT_PROVIDER, STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
STRIPE_WEBHOOK_SECRET, STRIPE_CONNECT_CLIENT_ID, STORAGE_PROVIDER, EMAIL_PROVIDER, QUEUE_DRIVER.
Segredos nunca são commitados. A chave secreta da Stripe é exclusivamente server-side, e o app se
recusa a subir com chave sk_live_ fora de produção.
Com APP_ENV=production o boot também é recusado se a configuração ainda aponta para os
substitutos de desenvolvimento, porque todos eles parecem saudáveis em runtime e perdem dados em
silêncio: PAYMENT_PROVIDER=mock, EMAIL_PROVIDER=console|preview, STORAGE_PROVIDER=local
(disco do container), APP_URL sem https, ou os segredos ainda iguais aos do .env.example.
npm run db:migrate # cria migration em desenvolvimento
npm run db:deploy # aplica migrations (CI/produção)
npm run db:seed # políticas de settlement e de taxa
npm run db:demo # dados de demonstração: catálogo + 30 dias de vendas (apenas dev)
npm run db:studio # Prisma Studio
npm run mongo:setup # cria collections e índices
Redis é usado hoje para rate limit do checkout; filas e locks distribuídos entram com o BullMQ.
Use sempre chaves de teste. Preencha STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY e
STRIPE_WEBHOOK_SECRET no .env, e mude PAYMENT_PROVIDER=stripe.
Encaminhe os webhooks com a Stripe CLI (não dependa de webhook configurado no dashboard para local):
stripe listen --forward-to localhost:3100/api/webhooks/stripe
O STRIPE_WEBHOOK_SECRET é o whsec_… que o comando imprime. Para eventos de contas conectadas,
acrescente --events account.updated,payout.paid,payout.failed ou use stripe listen --latest.
npm test # 68 testes
npm run verify # lint + typecheck + testes
Os testes de integração usam o banco hypei_test (criado pelo Docker Compose) e rodam contra
PostgreSQL de verdade — é assim que validamos transações, locks e restrições de unicidade.
Cobertura atual: money, cálculo de taxas, ledger (balanceamento, imutabilidade, idempotência), fluxo financeiro completo, projeção do balance, settlement, payouts (duplicidade, saldo insuficiente, concorrência, conta não verificada, falha e estorno), reembolsos (total, parcial, duplicado, acima do pago), disputas (reserva, ganha, perdida), idempotência de webhook (mesmo evento duas vezes ⇒ um único lançamento), assinatura inválida, eventos fora de ordem, isolamento de tenant, autorização, checkout e o provider Stripe (PaymentIntent, assinatura, fee real, mapeamentos).
src/
app/ rotas (App Router): landing, (app) dashboard, checkout público,
members, admin, onboarding, api/{auth,webhooks,health}
components/ ui/ (shadcn), layout/ (sidebar, topbar), shared/, brand/
modules/ um diretório por domínio — auth, organizations, products, offers,
checkout, customers, orders, payments, transactions, ledger, balances,
settlements, payouts, refunds, disputes, subscriptions, merchant-accounts,
members, analytics, webhooks, risk, compliance, taxes, fees, audit, integrations
(cada um com service, repository, schemas)
lib/ money/, errors/, env, logger/, clock, actions/, security/, idempotency/,
events/ (domain event, event bus, outbox), database/{postgres,mongo,redis},
providers/{payment,email,storage,queue}, stripe/, ui/
server/ repositories.ts, unit-of-work.ts, services.ts, container.ts, actions/
workers/ worker de background
prisma/ schema, migrations, seed
docker/ realm do Keycloak, init do Postgres
docs/architecture ADRs
tests/ unit/, integration/, support/
Funciona de ponta a ponta: login no Keycloak → onboarding → produto → oferta → checkout → compra no checkout público → webhook assinado → pagamento PAID → ledger → saldo pendente → settlement → saldo disponível → saque → payout pago, com o ledger fechando em cada etapa.
Também já implementados: assinaturas recorrentes (webhooks customer.subscription.* e invoice.*),
afiliados, cupons, editor da área de membros com upload, storage S3/R2/MinIO e as telas do /admin.
Imposto calcula e registra na venda, com o produtor como vendedor e a alíquota embutida no preço (ADR 008). O produtor configura o próprio regime em Configurações, e o tributo aparece no detalhe de cada venda — ao produtor, não ao comprador, por decisão registrada no ADR. Falta a emissão de NFS-e, que é onde a informação ao consumidor deve entrar, e os motores de UE (VAT/OSS) e EUA (sales tax).
Ainda não implementado (fases seguintes): AML — monitoramento e triagem; o KYC/KYB é delegado ao
Stripe Connect, que registra as pendências em Verificação —, API pública e os drivers de fila
BullMQ/Redis Streams (QUEUE_DRIVER aceita só memory).
Decisões arquiteturais estão registradas em docs/architecture.
A Fase 1 roda de ponta a ponta em desenvolvimento, mas estes pontos ainda separam isso de um ambiente de produção. Não são dívidas de código soltas: cada um tem consequência direta.
Impedem o funcionamento
npm run db:deploy. Na Vercel o
caminho usual é o comando de build, com um porém sério: preview deploys rodariam migrations
contra o banco que estiver em DATABASE_URL. Precisa ser resolvido com cuidado.QUEUE_DRIVER=memory tem implementação. Em serverless isso é pior do
que parece: trabalho agendado depois da resposta pode nunca rodar, porque a função é congelada.
Hoje o cron cobre o estrago — retryDue reclama eventos RECEIVED presos —, então nada se
perde, mas há latência de até um ciclo. A correção limpa é after() do next/server na rota
de webhook.Worker — resolvido na Vercel
O worker roda por cron, não por processo longo: vercel.json agenda
GET /api/cron/worker a cada minuto, e a rota chama a mesma runWorkerTick que o
npm run worker usa localmente. O agendamento por minuto exige plano Pro — no Hobby o cron
é diário, e settlement, outbox e retry de webhook ficariam com um dia de atraso.
A rota falha fechada: sem CRON_SECRET ela recusa tudo, e por isso o boot em produção também
é recusado sem esse segredo — senão o worker nunca rodaria e o saldo ficaria preso em pendente,
sem erro visível.
Identidade
EMAIL_PROVIDER da aplicação, e sem ela "esqueci minha senha" vira um beco sem saída: o
formulário aceita e o e-mail nunca chega. O npm run check:infra falha se o realm estiver sem
SMTP.verifyEmail está ligado, o que torna o SMTP acima obrigatório: realm de produção sem
remetente real não trava só a recuperação de senha, trava todo cadastro novo. O
npm run check:infra existe justamente para pegar isso antes.otpPolicyType ausente, nenhuma required action): o usuário não
consegue nem optar.start-dev e os redirectUris só aceitam localhost.
Produção exige realm próprio, kc start com hostname e TLS, e backup do banco de identidades.Infra e segurança
nosniff, Referrer-Policy e Permissions-Policy já
são enviados em todas as respostas (next.config.ts).images.remotePatterns libera https://** (next.config.ts), o que torna o
domínio um proxy de imagem aberto.Produto e regulatório
stripe-payment-provider.ts): a
conta do produtor precisa liquidar na moeda da venda. Aparece rápido com BR, UE e EUA juntos.