Diretório de meetups, talks e eventos de tecnologia por todo Portugal. Curado pelos próprios organizadores das comunidades.
| Camada | Tecnologia |
|---|---|
| Frontend | React 19, TypeScript, Tailwind CSS v4, Vite 7 |
| Backend | Express 5, Node.js (tsx) |
| Base de dados | Notion (via @notionhq/client) |
| Resend | |
| Package manager | pnpm |
# instalar dependências
pnpm install
# copiar variáveis de ambiente
cp .env.example .env.local
# editar .env.local com os valores reais
# iniciar API + frontend em simultâneo
pnpm devO frontend fica em http://localhost:5173 e o API em http://localhost:3001. O Vite faz proxy de /api/* para a porta 3001, por isso não há problemas de CORS em desenvolvimento.
| Variável | Obrigatória | Descrição |
|---|---|---|
NOTION_TOKEN |
✅ | Token de integração Notion (secret_…) |
NOTION_EVENTS_DB |
ID da base de dados de eventos (default já configurado) | |
NOTION_COMMUNITIES_DB |
ID da base de dados de comunidades (default já configurado) | |
ADMIN_PASSWORD |
✅ em produção | Password do painel admin (não pode ser o default ptcadmin) |
SESSION_SECRET |
✅ em produção | Segredo HMAC para cookies de sessão (mín. 32 caracteres, único) |
NODE_ENV |
production em deploy — ativa cookies secure, trust proxy e validação fail-fast |
|
API_PORT |
Porta do servidor API (default: 3001) |
|
RESEND_API_KEY |
API key Resend (necessária para magic links do portal) | |
RESEND_FROM |
Endereço de remetente (default: noreply@ptc.pt) |
|
SITE_URL |
URL público do site (default: http://localhost:5173) |
Em produção (
NODE_ENV=production) o servidor recusa arrancar seSESSION_SECRETouADMIN_PASSWORDestiverem em falta ou com os valores default. Gera um segredo com:node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
ptc-site/
├── server/
│ ├── index.ts # Entrada Express
│ ├── notion.ts # Cliente Notion, parsers, queries
│ ├── lib/
│ │ ├── email.ts # Envio de magic links via Resend
│ │ └── tokens.ts # Geração/consumo de tokens HMAC one-time
│ └── routes/
│ ├── auth.ts # Login/logout/session do admin
│ ├── events.ts # CRUD de eventos
│ ├── communities.ts # Listagem de comunidades
│ ├── config.ts # Configurações do site
│ └── portal.ts # Portal de líderes (magic link auth)
├── src/
│ ├── App.tsx # Router SPA (hash-based)
│ ├── pages/
│ │ ├── Home.tsx # Landing page
│ │ ├── Events.tsx # Lista de eventos
│ │ ├── Communities.tsx # Diretório de comunidades
│ │ ├── CommunityDetail.tsx
│ │ ├── About.tsx
│ │ ├── Admin.tsx # Painel de administração
│ │ └── Portal.tsx # Portal de líderes de comunidade
│ ├── components/ # Componentes reutilizáveis
│ └── lib/
│ ├── api.ts # Funções fetch para o backend
│ ├── siteConfig.ts # Config do site (localStorage + API)
│ └── tokens.ts # Utilitários de token (client-side)
└── vite.config.ts # Proxy /api → 3001
| Método | Path | Auth | Descrição |
|---|---|---|---|
POST |
/login |
— | Login com password; devolve cookie ptc_session |
POST |
/logout |
— | Apaga cookie de sessão |
GET |
/session |
— | Verifica se a sessão está ativa |
| Método | Path | Auth | Descrição |
|---|---|---|---|
GET |
/ |
— | Lista eventos aprovados e futuros (?all=true inclui não aprovados, ?past=true inclui passados) |
GET |
/:id |
— | Evento por ID |
POST |
/ |
Admin ou Portal | Criar evento |
PUT |
/:id |
Admin ou Portal | Editar evento (portal só pode editar os seus) |
DELETE |
/:id |
Admin ou Portal | Arquivar evento |
| Método | Path | Auth | Descrição |
|---|---|---|---|
GET |
/ |
— | Lista comunidades aprovadas |
| Método | Path | Auth | Descrição |
|---|---|---|---|
POST |
/magic-link |
Admin | Envia magic link para líder de comunidade |
GET |
/auth/:token |
— | Valida token e redireciona para /#portal |
GET |
/session |
— | Info da sessão portal atual |
POST |
/logout |
— | Termina sessão portal |
Password simples via ADMIN_PASSWORD. A sessão é um cookie ptc_session assinado com HMAC-SHA256. Válido 8 horas.
⚠️ TODO: Implementar Google OAuth antes de expor o painel publicamente — ver [memory/project_google_oauth_pending.md].
Magic link enviado por email. O admin vai a Admin → Portal e insere o email do líder. O sistema:
- Valida que o email existe na base Notion
CommunityLeaders - Gera token one-time (válido 24h, uso único)
- Envia email via Resend com link
GET /api/portal/auth/:token - Ao clicar, o servidor valida o token, cria cookie
ptc_portalassinado, e redireciona para/#portal
⚠️ O magic link viaja no path do URL (/api/portal/auth/:token). Configura o reverse proxy para não registar o path completo desta rota nos access logs (scrub/api/portal/auth/*).
As bases já estão pré-configuradas. Os IDs default estão em server/notion.ts:
| Base de dados | Env var | Campos relevantes |
|---|---|---|
| Events | NOTION_EVENTS_DB |
Name, Description, Venue, Date, Region, Format, Topic, Price, Approved, Community (relation) |
| Communities | NOTION_COMMUNITIES_DB |
Name, Slug, Region, Topic, Members, Founded, Description, Community Page, Logo URL, Status, Approved |
| Community Leaders | (hardcoded) | mail, Community (relation) |
Nota:
Community Leadersprecisa de uma propriedadeApproved(checkbox). Submissões de líderes ficam por aprovar até um admin as aprovar em Admin → Líderes.
pnpm build # compila o frontend para dist/
NODE_ENV=production pnpm start # arranca o API (tsx) — serve /api/* e, se dist/ existir, o SPAEm produção, um reverse proxy (nginx/Caddy) pode servir dist/ e encaminhar /api/*
para o processo Node, ou o próprio processo serve dist/.
O deploy usa App Platform (buildpack Node, sem Dockerfile). O spec está
versionado em .do/app.yaml: um único serviço web que corre
pnpm build e depois pnpm start — o processo Express serve /api/* e o SPA
(dist/) na porta injetada pela plataforma (PORT).
-
Instala e autentica o
doctl. -
Cria a app a partir do spec:
doctl apps create --spec .do/app.yaml
-
Define as variáveis secretas no componente
web(App → Settings → web → Environment Variables), tipo Encrypted, scope Run Time — não a nível da app (só owebprecisa delas):Variável Notas NOTION_TOKENtoken de integração Notion ADMIN_PASSWORDvalor forte, não-default (o servidor recusa arrancar caso contrário) SESSION_SECRET≥ 32 caracteres, único As não-secretas (
NODE_ENV,SITE_URL,RESEND_FROM,NOTION_EVENTS_DB,NOTION_COMMUNITIES_DB) já vêm no spec.SITE_URL=${APP_URL}resolve automaticamente para o URL público (*.ondigitalocean.app).RESEND_API_KEYé opcional — só é preciso para os magic links do portal de líderes. Sem ela o site e o admin funcionam; falha apenas o envio do email de convite. Fica para quando o domínio de email estiver verificado no Resend.
deploy_on_push: true está ativo — cada push para main no GitHub dispara um
build e deploy automáticos; alterações de código vão por aqui e não mexem
nas env vars.
⚠️ doctl apps update --specsubstitui toda a config da app: qualquer secret definido só no dashboard (e ausente do spec) é apagado no próximo update. Para mudar o próprio spec, volta a incluir os secrets antes de correr:doctl apps update <app-id> --spec .do/app.yaml
Adiciona o domínio no dashboard (ou um bloco domains: no spec). Como SITE_URL
usa ${APP_URL}, resolve sozinho; se precisares de o fixar, muda esse único valor.
⚠️ Logs: o magic link do portal viaja no path (/api/portal/auth/:token). Os logs de plataforma da DO podem registar paths de pedidos e não é possível limpá-los. Os tokens são de uso único e expiram em 24h, o que limita o risco.
pnpm dev # API + frontend em desenvolvimento
pnpm dev:vite # só frontend
pnpm dev:api # só API
pnpm build # build produção
pnpm typecheck # verificar tipos TypeScript