PersonalVirtual — Documentação Técnica e Roadmap de Implementação
Versão: 1.0
Status: Planejamento/Arquitetura
Modelo de negócio: SaaS B2B2C para Personal Trainers
Preço: R$ 69,90/mês (Personal)
Proposta de valor: Acompanhamento granular de treinos + curadoria de produtos via afiliados.
1. Visão Geral da Arquitetura
1.1 Stack proposta
| Camada |
Tecnologia |
Justificativa |
| Frontend + API |
Next.js 14+ (App Router) |
SSR para SEO de landing, ISR para páginas públicas, API Routes/BFF para não duplicar camada de backend |
| Banco de dados |
PostgreSQL 16 |
Relacional puro; histórico de execuções exige joins e agregações temporais |
| ORM |
Prisma ORM |
Type-safety, migrations versionadas e bom suporte a enums |
| Autenticação Personal |
Auth.js (NextAuth v5) — Credentials (Email/Senha) |
Padrão, com sessão httpOnly |
| Acesso do Aluno |
Hash Unique Token (URL dinâmica) |
Sem cadastro; token na URL (/a/<token>) — sem fricção |
| Pagamentos |
Stripe Checkout + Subscriptions + Webhooks |
Assinatura recorrente R$ 69,90 |
| PDF |
@react-pdf/renderer |
Geração de ficha planejada no servidor com templates React |
| Analytics |
Agregação SQL + painel próprio |
Sem dependência extra no MVP; evolução para PostHog |
| Deploy |
Vercel (app) + Neon/Supabase (Postgres) |
Serverless-friendly, webhooks Stripe como Serverless Functions |
1.2 Monorepo (estrutura de pastas)
personalvirtual/
├── apps/
│ └── web/ # Next.js (Painel Personal + Área do Aluno)
│ ├── app/
│ │ ├── (landing)/ # Landing page pública
│ │ ├── (auth)/ # Login/Registro do Personal
│ │ ├── painel/ # Área Desktop do Personal (autenticada)
│ │ └── a/[token]/ # Área Mobile do Aluno (hash, pública)
│ └── src/
│ ├── components/
│ ├── lib/ # prisma, stripe, auth
│ └── server/ # services (domínio)
├── packages/
│ ├── db/ # Prisma schema + migrations
│ └── shared/ # Tipos e utilidades compartilhadas
└── docs/
1.3 Modelo de autorização resumido
Personal -> sessão autenticada (Auth.js) -> acesso ao /painel
Aluno -> hash token (NONCE de 32+ chars) -> acesso a /a/:token
Assinatura do Personal deve estar "active" para o sistema liberar recursos.
2. Schema do Banco de Dados (PostgreSQL)
2.1 Diagrama ER (visão geral)
personals 1───* alunos
alunos 1───* treinos
treinos 1───* treino_exercicios
treino_exercicios 1───* execucoes
exercicios (catálogo, 1─* treino_exercicios)
personals 1───* recomendacoes_produtos
personals 1───* assinaturas
recomendacoes_produtos 1───* cliques_afiliados
2.2 DDL — Prisma Schema (equivalente)
// packages/db/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum StatusAssinatura {
TRIAL // trial 7 dias
ACTIVE // ativa
PAST_DUE // cartão com falha
CANCELED // cancelada
INCOMPLETE // checkout não finalizado
}
enum DiaDaSemana {
SEGUNDA
TERCA
QUARTA
QUINTA
SEXTA
SABADO
DOMINGO
}
model Personal {
id String @id @default(cuid())
nome String
email String @unique
senhaHash String // Argon2id
cref String?
avatarUrl String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
alunos Aluno[]
assinaturaAtualId String? @unique
assinaturaAtual Assinatura? @relation("AssinaturaAtual", fields: [assinaturaAtualId], references: [id])
assinaturas Assinatura[] @relation("HistoricoAssinaturas")
recomendacoes RecomendacaoProduto[]
}
model Aluno {
id String @id @default(cuid())
personalId String
personal Personal @relation(fields: [personalId], references: [id])
nome String
apelido String? // usado na URL amigável
email String?
telefone String?
dataNascimento DateTime?
hashToken String @unique // NONCE: crypto.randomBytes(32).toString('hex')
ativo Boolean @default(true)
observacoes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
treinos Treino[]
@@index([personalId])
}
model Exercicio {
id String @id @default(cuid())
nome String
grupo String? // Peito, Costas, Pernas...
imagemUrl String?
videoUrl String?
criadoPor String? @db.Uuid // personal que cadastrou (null = catálogo global)
treinoExercicios TreinoExercicio[]
}
model Treino {
id String @id @default(cuid())
personalId String
alunoId String
titulo String // "Treino A — Peito e Tríceps"
diaSemana DiaDaSemana?
dataAgendada DateTime? // treino avulso/datado
ordem Int @default(0) // ordenação da semana
ativo Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
personal Personal @relation(fields: [personalId], references: [id])
aluno Aluno @relation(fields: [alunoId], references: [id])
exercicios TreinoExercicio[]
@@index([alunoId, dataAgendada])
@@index([personalId])
}
model TreinoExercicio {
id String @id @default(cuid())
treinoId String
exercicioId String
ordem Int // ordem de execução
seriesPlanejadas Int // ex.: 4
repeticoes Int? // ex.: 10 (ou null p/ falha)
cargaSugeridaKg Decimal? @db.Decimal(6,2)
descansoSeg Int? @default(90)
anotacoes String?
treino Treino @relation(fields: [treinoId], references: [id])
exercicio Exercicio @relation(fields: [exercicioId], references: [id])
execucoes Execucao[]
@@unique([treinoId, exercicioId, ordem])
}
model Execucao {
id String @id @default(cuid())
treinoExercicioId String
serieNumero Int // 1..seriesPlanejadas
concluida Boolean @default(false)
cargaExecutadaKg Decimal? @db.Decimal(6,2)
repsExecutadas Int?
duracaoSeg Int?
observacao String?
realizadaEm DateTime @default(now())
treinoExercicio TreinoExercicio @relation(fields: [treinoExercicioId], references: [id])
@@unique([treinoExercicioId, serieNumero])
@@index([treinoExercicioId, realizadaEm])
}
model Assinatura {
id String @id @default(cuid())
personalId String
stripeCustomerId String?
stripeSubscriptionId String? @unique
plano String @default("MENSAL") // MENSAL, ANUAL
status StatusAssinatura @default(TRIAL)
precoEmCentavos Int
moeda String @default("brl")
dataInicio DateTime @default(now())
dataFim DateTime?
canceladoEm DateTime?
dataProximaCobranca DateTime?
personal Personal @relation("HistoricoAssinaturas", fields: [personalId], references: [id])
atualDe Personal? @relation("AssinaturaAtual")
@@index([personalId])
}
model RecomendacaoProduto {
id String @id @default(cuid())
personalId String
titulo String // "Whey da Growth"
descricao String?
linkAfiliado String // link com tag de afiliado (amzn.to/?tag=..., growth.ref=...)
imagemUrl String?
categoria String? // suplementos, acessorios, roupas
ordenacao Int @default(0)
ativo Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
personal Personal @relation(fields: [personalId], references: [id])
cliques CliqueAfiliado[]
@@index([personalId, ativo])
}
model CliqueAfiliado {
id String @id @default(cuid())
recomendacaoId String
alunoId String? // anônimo se null
ipHash String? // hash para contagem anônima
createdAt DateTime @default(now())
recomendacao RecomendacaoProduto @relation(fields: [recomendacaoId], references: [id])
@@index([recomendacaoId, createdAt])
}
2.3 Decisões de modelagem
Execucao por série (granularidade fina): cada série vira uma linha. Isso permite: saber quais séries foram puladas, comparar carga/rep planejada vs executada e gerar insights de aderência (requisito "Histórico").
TreinoExercicio (tabela pivô) em vez de series: o nº de séries é um atributo de configuração (seriesPlanejadas); as execuções concretas ficam em Execucao. Evita tabela de "séries planejadas" que duplicaria dados.
hashToken único no Aluno: URL pública sem fricção. Token revogável (regenerado ao trocar aparelho/perder acesso).
CliqueAfiliado anônimo: mede cliques sem capturar dados pessoais do aluno (LGPD-friendly) — apenas ipHash.
- Decimal para cargas: evitar erro de ponto flutuante em kg.
2.4 Migrations
- Prisma Migrate versionado em
packages/db/migrations/.
- Convenção:
db:migrate no CI e db:seed para catálogo inicial de exercícios.
3. Rotas da API
3.1 Autenticação (Personal)
| Método |
Rota |
Descrição |
| POST |
/api/auth/register |
Cria Personal + assinatura em TRIAL + cliente Stripe |
| POST |
/api/auth/login |
Login Credentials (Auth.js) |
| POST |
/api/auth/logout |
Encerra sessão |
| GET |
/api/auth/session |
Sessão atual |
| POST |
/api/auth/change-password |
Troca de senha |
3.2 Gestão de Alunos e Treinos (Painel Personal — autenticado)
| Método |
Rota |
Descrição |
| GET |
/api/personal/alunos |
Lista alunos (com hashToken p/ compartilhar) |
| POST |
/api/personal/alunos |
Cria aluno (gera hashToken) |
| PATCH |
/api/personal/alunos/:id |
Edita aluno |
| POST |
/api/personal/alunos/:id/regenerate-token |
Regenera hashToken (revoga antigo) |
| GET |
/api/personal/alunos/:id/treinos |
Treinos do aluno |
| POST |
/api/personal/alunos/:id/treinos |
Cria treino (com exercícios) |
| PATCH |
/api/personal/treinos/:treinoId |
Atualiza treino |
| DELETE |
/api/personal/treinos/:treinoId |
Remove treino |
| POST |
/api/personal/treinos/:treinoId/exercicios |
Adiciona exercício ao treino |
| GET |
/api/personal/alunos/:id/analytics |
Aderência do aluno (séries puladas) |
3.3 Área do Aluno — rota pública protegida por hash (NÃO requer sessão)
Todas as rotas abaixo validam :hashToken; o lookup retorna alunoId + personalId e nunca expõe dados de outros alunos.
| Método |
Rota |
Descrição |
| GET |
/api/p/a/:hashToken |
Dados básicos + treino do dia |
| GET |
/api/p/a/:hashToken/treino-do-dia |
Treino agendado p/ hoje + progresso |
| GET |
/api/p/a/:hashToken/treinos/:treinoId |
Detalhe do treino (exercícios + séries) |
| POST |
/api/p/a/:hashToken/treinos/:treinoId/execucoes |
Marca série: { treinoExercicioId, serieNumero, concluida, ... } |
| POST |
/api/p/a/:hashToken/treinos/:treinoId/concluir |
Marca treino como concluído (valida lógica hierárquica) |
| GET |
/api/p/a/:hashToken/historico |
Histórico de execuções do aluno |
| GET |
/api/p/a/:hashToken/recomendacoes |
"Dicas do seu Personal" (links curados) |
| POST |
/api/p/a/:hashToken/recomendacoes/:id/clique |
Registra clique (incrementa analytics) |
| GET |
/api/p/a/:hashToken/exportar-ficha |
Gera/baixa PDF da ficha planejada |
Segurança da rota pública:
- Token com entropia de 256 bits (crypto.randomBytes(32)).
- Rate limiting (ex.: 10 req/min por IP em /api/p/a/*).
- Logs não expõem o token em queries/URLs de log.
- Cache-Control: private para que o token não vaze via CDN.
3.4 Afiliados (Painel Personal)
| Método |
Rota |
Descrição |
| GET |
/api/personal/recomendacoes |
Lista links cadastrados |
| POST |
/api/personal/recomendacoes |
Cadastra produto/link (valida URL) |
| PATCH |
/api/personal/recomendacoes/:id |
Edita |
| DELETE |
/api/personal/recomendacoes/:id |
Remove |
| GET |
/api/personal/recomendacoes/:id/cliques |
Analytics de cliques (últimos 30 dias) |
| GET |
/api/personal/afiliados/resumo |
Total de cliques, CTR, receita estimada |
3.5 Assinatura e Webhooks
| Método |
Rota |
Descrição |
| POST |
/api/assinatura/checkout |
Cria sessão de Checkout do Stripe e redireciona |
| GET |
/api/assinatura/status |
Status da assinatura do Personal logado |
| POST |
/api/webhooks/stripe |
Recebe eventos (ver seção 5) |
| POST |
/api/assinatura/cancel |
Cancela (via portal de billing) |
3.6 Middleware de rota (Next.js)
// middleware.ts — proteção do /painel
export async function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
if (pathname.startsWith('/painel')) {
const session = await getToken({ req, secret: process.env.AUTH_SECRET });
if (!session) return NextResponse.redirect(new URL('/login', req.url));
const status = await getAssinaturaStatus(session.sub);
if (status !== 'ACTIVE' && status !== 'TRIAL') {
return NextResponse.redirect(new URL('/assinatura/vencida', req.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/painel/:path*'],
};
4. Estrutura do Frontend
4.1 Princípios de UX
- Personal (Desktop): painel denso, tabelas, multi-seleção, arrastar e soltar exercícios, gráficos.
- Aluno (Mobile-first): uma tela = um treino; checklist grande; barra de progresso; modo escuro opcional; botões mínimos de 48px de altura.
- Aba de Recomendações (non-invasive): nunca dentro do fluxo de treino. Acessível por ícone "Extras" no header ou aba secundária.
4.2 Árvore de componentes (Painel do Personal)
app/painel/
├── layout.tsx # Sidebar + topbar (Desktop)
├── page.tsx # Dashboard (indicadores de aderência)
├── alunos/
│ ├── page.tsx # Lista de alunos
│ ├── novo/page.tsx
│ └── [alunoId]/
│ ├── page.tsx # Detalhe + treinos
│ └── treinos/[treinoId]/edit/page.tsx
└── recomendacoes/
├── page.tsx # CRUD de links (lista + form)
└── [id]/analytics/page.tsx # Cliques por período
src/components/personal/
├── AlunoTable.tsx
├── TreinoEditor.tsx # Drag & drop de exercícios
├── ExercicioPicker.tsx
├── FichaPreview.tsx # Prévia da ficha p/ PDF
├── AderenciaChart.tsx
└── LinkForm.tsx
4.3 Árvore de componentes (Área do Aluno)
app/a/[token]/
├── layout.tsx # Tema escuro, header com modo escuro + ícone "Extras"
├── page.tsx # Hoje (treino do dia)
├── treinos/[treinoId]/page.tsx # Execução (checklist de séries)
└── extras/page.tsx # Aba de Recomendações ("Dicas do seu Personal")
src/components/aluno/
├── TreinoCard.tsx # Resumo do dia
├── ExercicioChecklist.tsx # Séries em checklist com botões grandes
├── SerieRow.tsx # Linha de série (marca concluída / pula)
├── ProgressRing.tsx / ProgressBar.tsx
├── ExtrasDrawer.tsx # Acesso secundário às recomendações
└── RecommendationCard.tsx
4.4 Posicionamento da aba de recomendações (UX)
┌─────────────────────────────┐
│ [Logo] [Treino] [Extras 🔧]│ <- ícone secundário, pequeno
├─────────────────────────────┤
│ TREINO DO DIA │
│ [barra de progresso] │
│ ▸ Peito │
│ Supino: ▢▢▢▢ │
│ Crucifixo: ▢▢▢ │
│ [Concluir Treino] │
└─────────────────────────────┘
- O fluxo de treino nunca exibe banners de afiliado.
ExtrasDrawer abre por cima (overlay) com cards: imagem, título, "Ver produto" (abre linkAfiliado em nova aba e dispara POST /clique).
5. Fluxo Stripe (Assinatura)
5.1 Diagrama de sequência (checkout)
Personal Next.js/API Stripe
| 1. POST /api/assinatura/checkout | |
|----------------------------------->| 2. createCheckoutSession |
| |----------------------------->|
| | 3. session (client_reference_id) |
| 4. redirect para checkout |<-----------------------------|
|<-----------------------------------| |
| 5. Usuário paga | |
| | 6. webhook checkout.session.completed |
| |<-----------------------------|
| | 7. atualiza Assinatura=ACTIVE + stripeSubscriptionId
| 8. GET /api/assinatura/status | |
|----------------------------------->| 9. retorna ACTIVE |
5.2 Eventos de webhook tratados
| Evento |
Ação |
checkout.session.completed |
Cria/atualiza Assinatura → ACTIVE, grava stripeCustomerId e stripeSubscriptionId |
customer.subscription.updated |
Sincroniza status (ACTIVE / PAST_DUE / CANCELED), dataProximaCobranca |
customer.subscription.deleted |
Status → CANCELED, canceladoEm |
invoice.payment_failed |
Status → PAST_DUE; envia e-mail de cobrança |
invoice.payment_succeeded |
Status → ACTIVE |
5.3 Controle de acesso (assinatura ativa)
[requisição no /painel ou rota do sistema]
│
▼
Assinatura.status ∈ { TRIAL, ACTIVE } ?
│ │
Sim Não
│ │
PERMITE USO Redireciona p/ página
"Assinatura vencida" +
link do Portal de Billing
- TRIAL (7 dias): permite uso completo; serve de prova de valor antes do cartão.
- PAST_DUE: grace period de 3 dias (cobrança re-tentada pelo Stripe); painel liberado em modo leitura, uploads/edição bloqueados.
- CANCELED: bloqueio total do painel; acesso do aluno pode ser mantido por leitura por 30 dias (decisão de negócio).
5.4 Validação de assinatura no servidor
// src/server/services/assinatura.ts
export async function hasAcessoAtivo(personalId: string): Promise<boolean> {
const assinatura = await prisma.assinatura.findFirst({
where: {
personalId,
status: { in: ['TRIAL', 'ACTIVE'] },
},
orderBy: { dataInicio: 'desc' },
});
if (!assinatura) return false;
// confere com o Stripe para desktops e mudanças fora do webhook
const sub = await stripe.subscriptions.retrieve(assinatura.stripeSubscriptionId);
return ['trialing', 'active'].includes(sub.status);
}
6. Snippets TypeScript
6.1 Cálculo de progresso do treino (lógica hierárquica)
// src/server/services/treino.ts
import type { TreinoExercicio, Execucao } from '@personalvirtual/db';
type Item = TreinoExercicio & { execucoes: Execucao[] };
export interface ProgressoTreino {
treinoId: string;
totalSeries: number;
concluidas: number;
percentual: number;
exercicios: Array<{
treinoExercicioId: string;
concluido: boolean; // todas as séries deste exercício concluídas
seriesConcluidas: number;
}>;
treinoConcluido: boolean; // TODAS as séries de TODOS os exercícios
}
export function calcularProgresso(
treinoId: string,
exercicios: Item[],
): ProgressoTreino {
let totalSeries = 0;
let concluidas = 0;
const mapeados = exercicios.map((te) => {
const feitas = te.execucoes.filter((e) => e.concluida);
const exercicioConcluido = feitas.length >= te.seriesPlanejadas;
totalSeries += te.seriesPlanejadas;
concluidas += Math.min(feitas.length, te.seriesPlanejadas);
return {
treinoExercicioId: te.id,
concluido: exercicioConcluido,
seriesConcluidas: feitas.length,
};
});
const percentual = totalSeries === 0 ? 0 : Math.round((concluidas / totalSeries) * 100);
return {
treinoId,
totalSeries,
concluidas,
percentual,
exercicios: mapeados,
treinoConcluido: concluidas >= totalSeries && totalSeries > 0,
};
}
6.2 Renderização das recomendações (Aba "Dicas do seu Personal")
// src/components/aluno/ExtrasDrawer.tsx
'use client';
import { useEffect, useState } from 'react';
import type { RecomendacaoProduto } from '@personalvirtual/db';
interface Props {
token: string;
alunoId: string;
onClose: () => void;
}
export function ExtrasDrawer({ token, alunoId, onClose }: Props) {
const [recomendacoes, setRecomendacoes] = useState<RecomendacaoProduto[]>([]);
const [carregando, setCarregando] = useState(true);
useEffect(() => {
fetch(`/api/p/a/${token}/recomendacoes`)
.then((r) => r.json())
.then(setRecomendacoes)
.finally(() => setCarregando(false));
}, [token]);
const aoClicar = async (id: string, link: string) => {
// registra o clique sem bloquear a navegação
fetch(`/api/p/a/${token}/recomendacoes/${id}/clique`, { method: 'POST' }).catch(() => {});
window.open(link, '_blank', 'noopener,noreferrer');
};
return (
<aside className="drawer" role="dialog" aria-label="Dicas do seu Personal">
<header>
<h2>Dicas do seu Personal</h2>
<button onClick={onClose} aria-label="Fechar">×</button>
</header>
{carregando && <p className="muted">Carregando…</p>}
<ul className="lista-recomendacoes">
{recomendacoes.map((r) => (
<li key={r.id}>
<button
className="recommendation-card"
onClick={() => aoClicar(r.id, r.linkAfiliado)}
>
{r.imagemUrl && <img src={r.imagemUrl} alt="" loading="lazy" />}
<div>
<strong>{r.titulo}</strong>
{r.descricao && <p>{r.descricao}</p>}
<span className="badge">{r.categoria}</span>
</div>
</button>
</li>
))}
</ul>
</aside>
);
}
7. Analytics de Aderência
Query principal — identificar exercícios/séries pulados por aluno:
SELECT
ex.nome AS exercicio,
COUNT(*) FILTER (WHERE NOT e.concluida) AS series_puladas,
COUNT(*) AS series_planejadas,
ROUND(100.0 * COUNT(*) FILTER (WHERE e.concluida) / COUNT(*), 1) AS aderencia_pct
FROM "TreinoExercicio" te
JOIN "Exercicio" ex ON ex.id = te."exercicioId"
LEFT JOIN "Execucao" e ON e."treinoExercicioId" = te.id
AND e."realizadaEm" >= now() - interval '30 days'
WHERE te."treinoId" = ANY(:treinosDoAluno)
GROUP BY ex.id, ex.nome
ORDER BY series_puladas DESC;
- Painel do Personal mostra: aderência semanal por aluno, exercícios com mais "pulos", evolução de carga.
- Insight acionável: notificação para o Personal sugerir troca de exercício ou ajuste de carga.
8. Estratégia de Monetização via Afiliados
8.1 Modelo de receita
| Fonte |
Comissão típica (Brasil) |
Papel da plataforma |
| Assinatura R$ 69,90 |
100% (receita principal) |
Plataforma |
| Afiliado — Suplementos (Growth, IntegralMédica, DarkLab) |
5–12% |
Personal cadastra link com ref e ganha comissão |
| Afiliado — Amazon (suplementos, acessórios, roupas) |
~2–5% |
Tag de afiliado do Personal |
| Marketplace (próxima fase) |
10–15% de take rate da plataforma |
Comissão intermediada pela plataforma |
- Diferencial de retenção: o Personal usa a plataforma para monetizar sua audiência — vira parte do fluxo de trabalho, reduz churn.
- Dados de curadoria: saber quais produtos convertem ajuda a negociar com marcas.
- Up-sell futuro: "Catálogo premium" onde a plataforma negocia comissões maiores em troca de volume (take rate).
8.3 Experiência non-invasive
- O Personal cadastra o link com a própria tag de afiliado (a comissão vai direto para ele).
- O aluno vê como "Dicas do seu Personal" — curadoria com autoridade do profissional.
- Nenhum anúncio dentro do fluxo de treino; apenas ícone "Extras".
- Regra de ouro:
recomendacoes só aparecem se o treino do dia já foi executado ou nos horários fora da janela de treino, para jamais interromper a execução.
8.4 Compliance
- Declaração nos termos de uso de que links são de afiliado.
rel="nofollow sponsored noopener" nos links.
- Analytics de clique sem PII (LGPD).
9. Roadmap de Desenvolvimento
Fase 0 — Fundação (Semanas 1–2)
- [ ] Setup monorepo (Next.js + Prisma + Postgres + CI).
- [ ] Auth.js com Credentials + registro com TRIAL.
- [ ] Schema completo + migrations + seed de catálogo.
Fase 1 — Núcleo do Produto (Semanas 3–5)
- [ ] CRUD de Alunos (com geração/regeneração de
hashToken).
- [ ] CRUD de Treinos +
TreinoExercicio (editor drag & drop).
- [ ] Área do Aluno
/a/:token: treino do dia + checklist de séries.
- [ ] Lógica hierárquica de conclusão + barra de progresso.
- [ ] Histórico de
Execucao.
Fase 2 — Monetização (Semanas 6–7)
- [ ] Stripe Checkout + Subscriptions + Webhooks + status.
- [ ] Middleware de assinatura ativa + página "assinatura vencida".
- [ ] CRUD de Recomendações + analytics de cliques.
- [ ] Aba "Extras" no app do aluno + registro de clique.
Fase 3 — Analytics e PDF (Semanas 8–9)
- [ ] Dashboard de aderência (query da seção 7) + gráficos.
- [ ] Exportação de ficha em PDF (react-pdf).
- [ ] Notificações (e-mail: cobrança, bem-vindo, treino pendente).
Fase 4 — Hardening (Semanas 10–12)
- [ ] Rate limiting em
/api/p/a/*, testes de segurança do token.
- [ ] Testes (Vitest + Playwright), cobertura de webhooks.
- [ ] Observabilidade (logs estruturados, erro tracking, uptime).
- [ ] Lighthouse/performance mobile + PWA (installável para o aluno).
Fase 5 — Escala (Pós-MVP)
- [ ] Multi-personal / academias (gerenciar treinadores).
- [ ] Programa de afiliados com take rate da plataforma.
- [ ] App nativo (React Native/Expo) reutilizando a API.
- [ ] Inteligência: recomendar ajuste de carga baseado no histórico.
10. Riscos e Mitigações
| Risco |
Mitigação |
| Churn do Personal |
TRIAL curto + valor de monetização afiliada como gancho |
| Token exposto/roubado |
Entropia 256 bits, rate limit, regeneração, HTTPS-only |
| Aluno não conclui treinos |
UX de 1 toque por série, lembretes, barra de progresso |
| Webhook Stripe falhando |
Idempotência por evento, fila de reprocessamento (retry + DLQ) |
| LGPD (dados do aluno) |
Dados mínimos, clique anônimo, política de privacidade |
| Dependência de comissões baixas |
A receita principal é a assinatura; afiliados são retenção + margem |