Links (Smart Link Shortener Premium)
Status: 🟡 Em fluxo (premium tier maturo, custom domains ativo) Code: backend/app/modules/links UI: frontend/src/app/(admin)/[slug]/admin/links Última revisão deste doc: 2026-05-13 por Felipe + Claude Dependências fortes: crm (scoring por link_type), attribution (touchpoints), tracking (clicks)
1. Identidade
O que faz (uma frase)
Encurtador de URLs premium com routing rules condicional, email gates, smart links com password, custom domains, A/B variants por canal, version history e geolocation tracking — gera URLs go.mandir.com.br/<slug> ou go.{custom}.com.br/<slug> que registram cada click.
Por que existe (negócio)
Encurtadores comerciais (bit.ly, rebrandly) cobram US$10-30/mês por features básicas e não cruzam com CRM/intelligence. Sem este módulo:
- Bit.ly stats fica isolado.
- Sem segmentação por canal (Instagram bio vs WhatsApp story).
- Sem captura de email no clique.
Com Links integrado:
- CRM scoring: link tipo "VIP" vale 50 pontos; "newsletter" vale 5.
- A/B per canal: link Instagram → variante A; link Twitter → variante B.
- Email gate: visitante precisa email pra ver conteúdo (lead magnet inteligente).
- Custom domain:
go.maisconsciente.com.br/aula(em vez de bitly). - Touchpoint attribution: click registra
contact_touchpointautomaticamente.
Status atual
- Premium tier maturo. Email gates, smart routing, custom domains todos ativos.
Próxima mudança: integração com mais providers GeoIP; analytics dashboard cross-link comparativo.
2. Cases de uso reais
Case 1: Link com email gate
Felipe cria link "PDF Mantras" → email_gate=true. Visitante clica → form pede email → submete → cria links_email_capture + links_email_session(expires_at=now+24h) → libera target_url. Próximo click do mesmo visitor_id antes de 24h pula form.
Case 2: Link com routing por device
Smart link "App Download": condição device_kind=android → google_play_url; device_kind=ios → app_store_url; default → landing genérica. Sem JS no client — backend decide.
Case 3: Custom domain
Felipe cadastra go.maisconsciente.com.br → backend gera verification_token → Felipe coloca CNAME no DNS. Backend verifica → verified_at=now. Links criados com domain="go.maisconsciente.com.br" resolvem nessa URL.
3. Oportunidades de negócio
- Link aggregator standalone: "Mandir Bio" tipo Linktree premium com automação CRM.
- A/B link testing: estatística automática "qual variante teve mais conversão".
- Email lead magnet workflow: form gate + sequência email automática + score CRM = micro-funnel completo.
- Custom domain marketplace: clientes vendem subdomínios (
go.<seu-brand>.com.br).
4. Arquitetura interna
Arquivos
models.py— 8 tabelas.routes.py— 27 endpoints + 1 público redirect.service.py— slug generation, click tracking, geolocation, routing rules parser, email gate.
Tasks
Sem Celery. Click tracking síncrono.
Adapter
- MaxMind GeoIP opcional — env
MAXMIND_ACCOUNT_ID+MAXMIND_LICENSE_KEY. Graceful fallback NULL country/city. - argon2 pra password hashing.
5. Tabelas (8)
links_link_type
Categorias (code, name, points_default, color, icon, sort_order, is_active). Ex: VIP=50pts, newsletter=5pts.
links_link
Shortlink principal.
| Coluna chave | Notas |
|---|---|
slug | UNIQUE per tenant (6-64 chars; auto-gerado fallback) |
target_url | Destino |
title / tags (JSONB) | |
utm_* (5 cols) | Auto-append na target_url |
tracking_events | JSONB smart rules |
routing_rules | JSONB DSL (device, country, dayofweek) |
password_hash | Optional argon2 |
email_gate (BOOL) | Lead magnet |
intersticial_seconds / intersticial_message | |
allowed_emails / max_sessions_per_email / email_session_ttl_hours (24 default) | |
email_session_window_start / _end | Soft constraints |
domain | Custom domain ou NULL = go.mandir.com.br |
link_type_id | FK |
custom_points | Override scoring |
crm_filters | JSONB |
click_cap / click_cap_per_visitor / click_cap_per_visitor_window_hours (24 default) | Rate limits |
is_active / expires_at | |
click_count / last_click_at | Cache |
links_share
Variant per canal (link_id, code=8 chars unique global, label, channel, ref_name, click_count, last_click_at).
links_click
Evento click.
| Coluna | Notas |
|---|---|
link_id | FK |
visitor_id (UUID) | Cookie-based |
ip (INET) / user_agent / referrer | |
country / city | MaxMind |
device_kind | mobile_ios / mobile_android / desktop / etc. |
extra | JSONB |
created_at (idx) |
links_email_capture
Email gate harvest.
| Coluna | Notas |
|---|---|
link_id / email / name / visitor_id / ip / user_agent |
links_email_session
Sessão ativa email gate.
| Coluna | Notas |
|---|---|
link_id / email / visitor_id / ip / user_agent | |
created_at / expires_at | TTL 24h default |
released_at / released_reason | Quando admin libera manualmente |
links_custom_domain
| Coluna | Notas |
|---|---|
domain | UNIQUE GLOBAL (não por tenant!) |
verification_token / verified_at | CNAME flow |
is_active |
links_link_version
Histórico snapshots pré-PATCH (link_id, version_num, snapshot JSONB, created_at).
Relacionamentos cross-módulo
| Direção | Outro módulo | Como |
|---|---|---|
| ↘ Escreve | attribution | append_touchpoint(channel='website', source_module='links') em click |
| ↘ Emite event | tracking | links.click |
| ↗ Lê | crm_scoring_rule | Points por link_type aplicado em CRM |
6. API / Endpoints (~27)
Prefixo /api/links.
Link types (3)
| Método | Rota | O que faz |
|---|---|---|
| GET / POST / PATCH / DELETE | /types | CRUD |
Links (5)
| Método | Rota | O que faz |
|---|---|---|
| GET / POST / GET{id} / PATCH / DELETE | /links | CRUD |
Clicks / Health / Versions
| Método | Rota | O que faz |
|---|---|---|
| GET | /links/{id}/clicks | Lista filtered |
| GET | /links/{id}/health | Status |
| GET | /links/{id}/versions | Histórico |
| POST | /links/{id}/versions/{n}/restore | Restore como new version |
Email gates
| Método | Rota | O que faz |
|---|---|---|
| GET | /links/{id}/captures | List CSV export |
| GET | /links/{id}/sessions | List ativas |
| POST | /links/{id}/sessions/{id}/release | Manual release |
Shares
| Método | Rota | O que faz |
|---|---|---|
| GET / POST / DELETE | /links/{id}/shares | CRUD variants |
Domains (3)
| Método | Rota | O que faz |
|---|---|---|
| GET / POST | /domains | List / Create |
| POST | /domains/{id}/verify | Check CNAME |
| DELETE | /domains/{id} | Delete |
Simulate (1)
| Método | Rota | O que faz |
|---|---|---|
| POST | /links/{id}/simulate | Dry-run routing rules |
Public redirect (1)
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| GET | /{slug} (no /api) | Público | 302 redirect (registra click) |
7. Configuração
Env vars
| Var | Propósito |
|---|---|
MAXMIND_ACCOUNT_ID | GeoIP optional |
MAXMIND_LICENSE_KEY | GeoIP optional |
LINKS_DEFAULT_DOMAIN | go.mandir.com.br (hardcoded fallback) |
Custom domain DNS setup
Cliente coloca CNAME go.{custom}.com.br → *.go.mandir.com.br. Backend verifica via verification_token em DNS TXT.
Kill switches
links_link.is_active=false→ 410 Gone.links_link.expires_at<now→ 410 Gone.click_capexcedido → 429 Too Many Requests.
8. Operações
Como criar link com email gate
- UI
/admin/links→ "Novo" → form (slug, target_url, email_gate=true, allowed_emails opcional). - Compartilhar URL
https://go.mandir.com.br/<slug>. - Visitante click → form email → submete → libera.
Troubleshooting
Sintoma: Slug duplicado em conflict
Causa: Auto-gerado bateu com existente (raro 6 chars random). Fix: Tenta novamente (até 5x) ou user passa slug manual.
9. Limitações e débitos técnicos
| # | Item |
|---|---|
| 1 | Click cap não atomic — race condition (acceptable pra lead gates) |
| 2 | email_session window soft — não bloqueia, só metadata |
| 3 | Share code uniqueness GLOBAL — não per tenant (design choice) |
| 4 | Sem time-travel rollback — version restore = new version |
| 5 | MaxMind opcional — sem ele, country/city = NULL |
10. Histórico
Premium tier construído iterativamente:
- Sprint 1 — basic shortener.
- Sprint 2 — email gates + smart links.
- Sprint 3 — custom domains.
- Sprint 4 — share codes + attribution.
- Sprint 5 — tracking integration.