Documentação Técnica — Backend Django
Clone de Community Platform (estilo Circle.so) Django 6 · Python 3.14 · PostgreSQL · Redis · Celery · Channels
Sumário
- Visão Geral da Arquitetura
- Stack Tecnológica
- Estrutura de Apps
- Modelos de Dados
- Mapa de Relacionamentos
- Sistema de Permissões
- Middlewares
- APIs
- WebSockets
- Gamificação
- Sinais (Signals)
- Configurações
- Commands de Gestão
- Endpoints Principais
1. Visão Geral da Arquitetura
O sistema é uma plataforma multi-tenant baseada em subdomínios. Cada Community é resolvida a partir do header HTTP Host. Todo conteúdo é escopado a uma comunidade via ForeignKey explícita em todos os modelos.
Fluxo de Requisição
HTTP Request (Host: minha-comunidade.example.com)
│
▼
TenantResolutionMiddleware
│ 1. Extrai host do request
│ 2. Busca Community por slug ou custom_domain
│ 3. Anexa request.community
│ 4. Busca Membership (se autenticado)
│ 5. Anexa request.membership
│
▼
DRF View / WebSocket Consumer
│
▼
Permissões → Serializers → Models → DatabaseCamadas de Acesso
O sistema implementa três camadas de acesso independentes:
| Camada | Pergunta | Implementação |
|---|---|---|
| Visibilidade | "Posso ver este espaço?" | SpaceQuerySet.visible_to() |
| Acesso | "Posso ler/escrever conteúdo?" | Space.is_accessible_to() + HasSpaceAccess |
| Moderação | "Posso excluir/trancar/banir?" | IsAuthorOrModerator + SpaceModerator |
2. Stack Tecnológica
| Componente | Tecnologia | Função |
|---|---|---|
| Framework | Django 6 | MVC backend |
| Linguagem | Python 3.14 | Runtime |
| Banco | PostgreSQL | Dados persistentes |
| Cache/Broker | Redis | Cache, sessões, Celery broker, WebSocket tokens |
| Autenticação | django-allauth | Login, registro, email verification |
| API REST | Django Rest Framework + JWT | Endpoints JSON |
| Protocolo MCP | FastMCP | Servidor MCP para integração com assistentes de IA |
| Tarefas | Celery + django-celery-beat | Jobs em background com arquitetura de workers segregados por perfis de carga (Light, Heavy, Import, Stems, Dubbing, Diarization) |
| WebSockets | Django Channels | Chat ao vivo e DMs |
| Frontend | Tailwind v4 + DaisyUI + HTMX + Alpine.js | Interface |
| Bundler | Vite via django-vite | Assets JS/CSS |
| Armazenamento | S3 (MinIO-compatible) | Upload de imagens |
| OpenAPI | drf-spectacular | Schema + Swagger UI |
3. Estrutura de Apps
apps/
├── utils/ # BaseModel abstrato (created_at, updated_at)
├── users/ # Modelo CustomUser, autenticação, perfil
├── communities/ # Multi-tenancy: Community, Membership, permissões
├── payments/ # Paywall, Checkout, Transações, Assinaturas e Gateways
├── spaces/ # Conteúdo: Spaces, Posts, Comentários, Eventos, Cursos, Chat, DMs, Produtos Digitais
├── headless/ # API compatível com Circle.so (shim REST)
├── downloads/ # Analytics de downloads de arquivos (ADR 0016)
├── mcp_server/ # Servidor MCP (FastMCP) para integração com agentes de IA
└── web/ # Landing page, management commands, template tagsMapa de Responsabilidades
| App | Responsabilidade |
|---|---|
utils | BaseModel abstrato estendido por todos os modelos concretos |
users | CustomUser com avatar, display_name, email verification |
communities | Tenancy (Community), adesão (Membership), gamificação, API keys, grupos de acesso, campos de perfil customizados |
payments | Gestão de preços (Paywall), Checkouts, Pagamentos, Assinaturas recorrentes, integração de Gateways (Asaas, Fake) e Webhooks |
spaces | Todo o conteúdo: Posts, Comentários, Eventos, Cursos, Chat, DMs, Imagens, Reações, Reports, Moderação, Produtos Digitais (ProductItem) |
media | Pipeline de mídia (vídeo/áudio/imagem): upload, transcodificação HLS, transcrição (Deepgram), diarização, resumo/chapters/highlights por LLM (ADR 0004), Stem Separation (Demucs), Dublagem (tradução+TTS), Highlights com IA e cover frame por visão computacional (ADR 0018) |
streaming | Analytics de playback (MediaPlaybackSession/Heartbeat) — separado de media (que só processa/produz o arquivo), ver ADR 0009 |
headless | API shim com shape JSON compatível com Circle.so |
downloads | Analytics de downloads (FileDownloadEvent): registro nos 3 fluxos (Drive, anexos de aula, produtos digitais) + sumários por alvo e dashboard admin (ADR 0016) |
mcp_server | Servidor MCP (FastMCP): autenticação via MCPApiKey, tools para gerenciamento por IA |
web | Páginas estáticas, management commands (seed, bootstrap_celery) |
4. Modelos de Dados
4.1 BaseModel (Abstract)
Todos os modelos concretos (exceto CustomUser) herdam deste BaseModel.
| Campo | Tipo | Descrição |
|---|---|---|
created_at | DateTimeField(auto_now_add) | Data de criação |
updated_at | DateTimeField(auto_now) | Última atualização |
4.2 CustomUser
Extends AbstractUser.
| Campo | Tipo | Descrição |
|---|---|---|
avatar | FileField | Foto de perfil — re-encodada para WebP 256px no upload (via apps.media.services.optimize), salvo em profile-pictures/{uuid}.webp |
Propriedades:
get_display_name()→ nome completo, ou email, ou usernameavatar_url→ URL do avatar ou fallback Gravatarhas_verified_email→ verifica allauth EmailAddress
4.3 Community (Tenant Principal)
Cada comunidade é um tenant isolado.
| Campo | Tipo | Descrição |
|---|---|---|
name | CharField(255) | Nome da comunidade |
slug | SlugField(63, unique) | Subdomínio (validado contra subdomínios reservados) |
custom_domain | CharField(255, unique, nullable) | Domínio próprio opcional |
custom_domain_verified | BooleanField(default=False) | Se o domínio próprio foi verificado |
owner | ForeignKey → CustomUser(PROTECT) | Dono da comunidade |
invite_only | BooleanField(default=True) | Se é apenas por convite |
is_active | BooleanField(default=True) | Se a comunidade está ativa |
description | TextField(blank) | Descrição da comunidade |
default_language | CharField(10, default="pt-BR") | Idioma primário padrão da comunidade |
available_languages | JSONField(default=list) | Lista de códigos de idiomas suportados (ex: ["pt-BR", "en-US"]) |
is_multilingual | BooleanField(default=False) | Se os recursos multilíngue e traduções estão ativos no tenant |
timezone | CharField(50, default="America/Sao_Paulo") | Fuso horário padrão da comunidade |
currency | CharField(3, default="BRL") | Moeda padrão para cobranças/paywalls |
i18n | JSONField(default=dict) | Dicionário de traduções de campos dinâmicos por código de idioma |
nav_config | JSONField(default=list) | Lista esparsa de overrides dos itens fixos da navbar (feed/library/courses/products/live/leaderboard): [{key, enabled, order, label, icon_type, icon_value}, ...]. Chave ausente = usa o padrão. Editado em /settings/community/navigation (admin only). |
Constraints: UNIQUE(slug), UNIQUE(custom_domain)
4.4 Membership (Adesão Usuário ↔ Comunidade)
| Campo | Tipo | Descrição |
|---|---|---|
user | ForeignKey → CustomUser(CASCADE) | related_name: memberships |
community | ForeignKey → Community(CASCADE) | related_name: memberships |
role | CharField | admin, moderator, member |
status | CharField | active, invited, banned |
invite_token | CharField(64, unique, nullable) | Token de convite |
invited_by | ForeignKey → CustomUser(SET_NULL, nullable) | Quem convidou |
joined_at | DateTimeField(nullable) | Data de adesão |
muted_until | DateTimeField(nullable) | Silenciado até (None = não silenciado, data futura = silenciado permanentemente) |
Constraints: UNIQUE(user, community), INDEX(community, role), INDEX(community, status)
Métodos:
generate_invite_token()→ gera token únicoaccept_invite()→ aceita convite, define status=activeis_muted()→ verifica se está silenciado
4.5 PointTransaction (Gamificação)
| Campo | Tipo | Descrição |
|---|---|---|
membership | ForeignKey → Membership(CASCADE) | related_name: point_transactions |
action | CharField | post_created, comment_created, lesson_completed |
points | PositiveIntegerField | Pontos concedidos |
content_type | ForeignKey → ContentType(CASCADE, nullable) | Para GenericFK |
object_id | PositiveBigIntegerField(nullable) | Para GenericFK |
target | GenericForeignKey | Alvo da transação |
Constraints: UNIQUE(membership, action, content_type, object_id)
Validação: clean() verifica que o target pertence à mesma comunidade.
4.6 ProfileFieldDefinition (Schema de Perfil Customizado)
| Campo | Tipo | Descrição |
|---|---|---|
community | ForeignKey → Community(CASCADE) | |
name | CharField(100) | Label exibido |
key | SlugField(100) | Identificador único por comunidade |
field_type | CharField | text, textarea, number, url, date, choice, multi_choice |
options | JSONField(default=list) | Opções para choice/multi_choice |
is_required | BooleanField(default=False) | Se é obrigatório |
order | PositiveIntegerField(default=0) | Ordem de exibição |
4.7 ProfileFieldValue (Dados do Perfil Customizado)
| Campo | Tipo | Descrição |
|---|---|---|
membership | ForeignKey → Membership(CASCADE) | related_name: profile_field_values |
field_definition | ForeignKey → ProfileFieldDefinition(CASCADE) | |
value | JSONField(nullable) | Valor preenchido |
Constraints: UNIQUE(membership, field_definition)
4.8 AccessGroup (Grupo — Controle de Acesso Granular)
| Campo | Tipo | Descrição |
|---|---|---|
community | ForeignKey → Community(CASCADE) | |
name | CharField(100) | Nome do grupo |
slug | SlugField(100) | Slug único por comunidade |
description | TextField(blank) | Descrição |
members | M2M → Membership (through AccessGroupMembership) | Membros do grupo |
4.9 AccessGroupMembership (Through Model)
| Campo | Tipo | Descrição |
|---|---|---|
access_group | ForeignKey → AccessGroup(CASCADE) | |
membership | ForeignKey → Membership(CASCADE) |
Constraints: UNIQUE(access_group, membership)
4.10 APIKey
| Campo | Tipo | Descrição |
|---|---|---|
community | ForeignKey → Community(CASCADE) | related_name: api_keys |
scopes | JSONField(default=list) | Escopos de permissão |
last_used_at | DateTimeField(nullable) | Último uso |
created_by | ForeignKey → CustomUser(SET_NULL, nullable) | Quem criou |
4.11 SpaceGroup (Seção — Agrupamento na Sidebar)
| Campo | Tipo | Descrição |
|---|---|---|
community | ForeignKey → Community(CASCADE) | |
name | CharField(100) | Nome do grupo |
slug | SlugField(100) | Slug único por comunidade |
order | PositiveIntegerField(default=0) | Ordem na sidebar |
4.12 Space (Container Principal de Conteúdo)
O modelo central da plataforma. Cada Space define o tipo de conteúdo que armazena.
| Campo | Tipo | Descrição |
|---|---|---|
community | ForeignKey → Community(CASCADE) | |
space_group | ForeignKey → SpaceGroup(CASCADE) | Agrupamento na sidebar |
name | CharField(100) | Nome do espaço |
slug | SlugField(100) | Slug único por comunidade |
description | TextField(blank) | Descrição |
space_type | CharField | basic, chat, event, course, members, image |
access_level | CharField | public, private, secret |
access_groups | M2M → AccessGroup(blank) | Grupos com acesso |
order | PositiveIntegerField(default=0) | Ordem na sidebar |
Manager customizado: SpaceQuerySet com visible_to(membership) — implementa regras de visibilidade:
public→ listado para todosprivate→ listado mas acesso restritosecret→ oculto a menos que admin/mod ou granted via access_groups
Método: is_accessible_to(membership) — verificação de leitura/escrita distinta da visibilidade.
4.13 Post
| Campo | Tipo | Descrição |
|---|---|---|
space | ForeignKey → Space(CASCADE) | Apenas em spaces basic |
community | ForeignKey → Community(CASCADE) | |
author | ForeignKey → Membership(PROTECT) | |
body | TextField | Conteúdo (Markdown/plain text) |
is_pinned | BooleanField(default=False) | Fixado no topo |
is_locked | BooleanField(default=False) | Trancado (sem novos comentários) |
tags | M2M → Tag(blank) | Tags do post |
reactions | GenericRelation → Reaction |
Ordering: ["-is_pinned", "-created_at"]
Validação: Posts só podem ser criados em spaces do tipo basic.
4.14 Comment
| Campo | Tipo | Descrição |
|---|---|---|
post | ForeignKey → Post(CASCADE) | |
community | ForeignKey → Community(CASCADE) | |
author | ForeignKey → Membership(PROTECT) | |
parent | ForeignKey → self(CASCADE, nullable) | Aninhamento máximo de 1 nível |
body | TextField | Conteúdo |
reactions | GenericRelation → Reaction |
Constraints: Respostas com máximo 1 nível de profundidade. Resposta deve pertencer ao mesmo post que o pai.
4.15 Event
| Campo | Tipo | Descrição |
|---|---|---|
space | ForeignKey → Space(CASCADE) | Apenas em spaces event |
community | ForeignKey → Community(CASCADE) | |
host | ForeignKey → Membership(PROTECT) | |
title | CharField(200) | Título do evento |
description | TextField(blank) | Descrição |
starts_at | DateTimeField | Data/hora de início |
ends_at | DateTimeField | Data/hora de término (validado: deve ser após starts_at) |
timezone | CharField(64) | Timezone (validado contra available_timezones()) |
virtual_meeting_url | URLField(blank) | URL da reunião virtual |
location | CharField(255, blank) | Local físico |
4.16 RSVP
| Campo | Tipo | Descrição |
|---|---|---|
event | ForeignKey → Event(CASCADE) | |
membership | ForeignKey → Membership(CASCADE) | |
status | CharField | going, interested, not_going |
Constraints: UNIQUE(event, membership)
4.17 Tag
| Campo | Tipo | Descrição |
|---|---|---|
community | ForeignKey → Community(CASCADE) | |
name | CharField(50) | Nome da tag |
slug | SlugField(50) | Slug único por comunidade |
4.18 Reaction (Polimórfico)
| Campo | Tipo | Descrição |
|---|---|---|
membership | ForeignKey → Membership(CASCADE) | |
content_type | ForeignKey → ContentType(CASCADE) | |
object_id | PositiveBigIntegerField | |
target | GenericForeignKey | Post, Comment ou Image |
emoji | CharField | like, love, celebrate, support, insightful, curious |
Constraints: UNIQUE(membership, content_type, object_id). Só é possível reagir a Post, Comment ou Image.
4.19 Image
| Campo | Tipo | Descrição |
|---|---|---|
space | ForeignKey → Space(CASCADE) | Apenas em spaces image |
community | ForeignKey → Community(CASCADE) | |
author | ForeignKey → Membership(PROTECT) | |
file | FileField | Imagem otimizada (WebP, máx. 1200px, EXIF-transpose) — re-encodada no serializer via apps.media.services.optimize, salva em gallery-images/{uuid}.webp |
caption | CharField(280, blank) | Legenda |
reactions | GenericRelation → Reaction |
4.20 Module (Seção do Curso)
| Campo | Tipo | Descrição |
|---|---|---|
space | ForeignKey → Space(CASCADE) | Apenas em spaces course |
community | ForeignKey → Community(CASCADE) | |
title | CharField(200) | Título do módulo |
description | TextField(blank) | Descrição |
order | PositiveIntegerField(default=0) | Ordem |
4.21 Lesson
| Campo | Tipo | Descrição |
|---|---|---|
module | ForeignKey → Module(CASCADE) | |
space | ForeignKey → Space(CASCADE) | Denormalizado do módulo |
community | ForeignKey → Community(CASCADE) | |
title | CharField(200) | Título da aula |
body | TextField(blank) | Conteúdo da aula |
order | PositiveIntegerField(default=0) | Ordem |
4.22 LessonProgress
| Campo | Tipo | Descrição |
|---|---|---|
lesson | ForeignKey → Lesson(CASCADE) | |
membership | ForeignKey → Membership(CASCADE) | |
completed_at | DateTimeField(nullable) | Data de conclusão |
Constraints: UNIQUE(lesson, membership)
4.23 ChatMessage
| Campo | Tipo | Descrição |
|---|---|---|
space | ForeignKey → Space(CASCADE) | Apenas em spaces chat |
community | ForeignKey → Community(CASCADE) | |
author | ForeignKey → Membership(PROTECT) | |
body | TextField | Mensagem |
edited_at | DateTimeField(nullable) | Data de edição |
deleted_at | DateTimeField(nullable) | Soft-delete (tombstone para WebSocket) |
4.24 DMThread
| Campo | Tipo | Descrição |
|---|---|---|
community | ForeignKey → Community(CASCADE) | |
membership_a | ForeignKey → Membership(CASCADE) | Membro com menor ID |
membership_b | ForeignKey → Membership(CASCADE) | Membro com maior ID |
Constraints: UNIQUE(membership_a, membership_b), CHECK(membership_a < membership_b)
Manager: DMThreadManager.get_or_create_between(community, m1, m2) — ordenação canônica garante exatamente um thread por par.
Método: has_participant(membership)
4.25 DMMessage
| Campo | Tipo | Descrição |
|---|---|---|
thread | ForeignKey → DMThread(CASCADE) | |
sender | ForeignKey → Membership(PROTECT) | |
body | TextField | Mensagem |
edited_at | DateTimeField(nullable) | Data de edição |
deleted_at | DateTimeField(nullable) | Soft-delete |
4.26 Report (Polimórfico)
| Campo | Tipo | Descrição |
|---|---|---|
reporter | ForeignKey → Membership(PROTECT) | |
community | ForeignKey → Community(CASCADE) | |
content_type | ForeignKey → ContentType(CASCADE) | |
object_id | PositiveBigIntegerField | |
target | GenericForeignKey | Post, Comment, Image ou ChatMessage |
reason | CharField | spam, harassment, off_topic, other |
details | TextField(blank) | Detalhes adicionais |
status | CharField | open, resolved, dismissed |
resolved_by | ForeignKey → Membership(SET_NULL, nullable) | |
resolved_at | DateTimeField(nullable) |
Constraints: UNIQUE(reporter, content_type, object_id)
4.27 SpaceModerator
| Campo | Tipo | Descrição |
|---|---|---|
space | ForeignKey → Space(CASCADE) | |
membership | ForeignKey → Membership(CASCADE) |
Constraints: UNIQUE(space, membership)
Concede direitos de moderação de conteúdo (deletar/trancar) em um Space específico — escopo mais estreito que a role admin/moderator da comunidade.
4.28 Bookmark
| Campo | Tipo | Descrição |
|---|---|---|
user | ForeignKey → CustomUser(CASCADE) | related_name: bookmarks |
community | ForeignKey → Community(CASCADE) | related_name: bookmarks |
post | ForeignKey → Post(CASCADE) | related_name: bookmarks |
Constraints: UNIQUE(user, post)
4.29 PostFollower
| Campo | Tipo | Descrição |
|---|---|---|
user | ForeignKey → CustomUser(CASCADE) | related_name: post_follows |
post | ForeignKey → Post(CASCADE) | related_name: followers |
Constraints: UNIQUE(user, post)
4.30 ChatRoom
| Campo | Tipo | Descrição |
|---|---|---|
community | ForeignKey → Community(CASCADE) | related_name: chat_rooms |
uuid | UUIDField(unique) | Identificador para URLs |
kind | CharField | direct, group_chat |
name | CharField(255, blank) | Nome da sala (grupos) |
created_by | ForeignKey → Membership(SET_NULL, nullable) | Quem criou |
Ordering: -created_at
4.31 ChatRoomMember
| Campo | Tipo | Descrição |
|---|---|---|
chat_room | ForeignKey → ChatRoom(CASCADE) | related_name: members |
membership | ForeignKey → Membership(CASCADE) | related_name: chat_room_memberships |
last_read_at | DateTimeField(nullable) | Última leitura |
Constraints: UNIQUE(chat_room, membership)
4.32 ChatRoomMessage
| Campo | Tipo | Descrição |
|---|---|---|
chat_room | ForeignKey → ChatRoom(CASCADE) | related_name: messages |
sender | ForeignKey → Membership(SET_NULL, nullable) | related_name: chat_room_messages |
body | TextField(blank) | Texto da mensagem |
rich_text_body | JSONField(nullable) | Formato rico (Tiptap) |
parent_message | ForeignKey → self(SET_NULL, nullable) | Para threads |
edited_at | DateTimeField(nullable) | Data de edição |
deleted_at | DateTimeField(nullable) | Soft-delete |
Ordering: created_at
4.33 MCPApiKey
Chave de API para autenticação de assistentes de IA no Servidor MCP. Vinculada a uma Membership (par Usuário + Comunidade), garantindo o escopo do tenant e as permissões do membro.
| Campo | Tipo | Descrição |
|---|---|---|
membership | ForeignKey → Membership(CASCADE) | related_name: mcp_api_keys |
name | CharField(100, default="MCP key") | Nome de identificação da chave |
prefix | CharField(16, db_index) | Prefixo em texto plano para identificação (primeiros 16 chars) |
key_hash | CharField(64, unique) | Hash SHA-256 da chave bruta |
last_used_at | DateTimeField(nullable) | Data do último uso |
revoked_at | DateTimeField(nullable) | Data de revogação |
Propriedades e Métodos:
is_active→Trueserevoked_atforNonecreate_key(membership, name)→ gera nova chavemcp_<token>e armazena o hashhash_key(raw_key)→ calcula o hash SHA-256 da chave brutamark_used()→ atualizalast_used_atrevoke()→ definerevoked_atpara a data atual
4.34 CatalogSyncLog
Log de execuções do command sync_catalog (ver seção 13), que sincroniza Community/Space (course)/Module/Lesson para o serviço externo catalog em _internal/apps/catalog (Neon Postgres separado, fora do DATABASES deste projeto — sem FK real, ligação só pelo run_id). Espelhado do lado do catalog na tabela sync_log.
| Campo | Tipo | Descrição |
4.35 MediaPlaybackSession
Registra uma sessão de reprodução de vídeo/áudio por um usuário ou visitante.
| Campo | Tipo | Descrição |
|---|---|---|
media | ForeignKey → Media(CASCADE) | Mídia reproduzida |
user | ForeignKey → CustomUser(SET_NULL, nullable) | Usuário autenticado (se houver) |
session_id | UUIDField(unique, db_index) | UUID da sessão gerado pelo player |
device_type | CharField(50) | desktop, mobile, etc |
browser | CharField(50) | User-agent/Navegador |
total_watch_time | FloatField(default=0.0) | Tempo total assistido em segundos |
max_position | FloatField(default=0.0) | Maior posição alcançada no vídeo (s) |
completion_rate | FloatField(default=0.0) | Taxa de conclusão (0.0 a 1.0) |
is_completed | BooleanField(default=False) | Indica se o vídeo foi concluído (>= 90%) |
4.36 MediaPlaybackHeartbeat
Log pontual de heartbeat/ping enviado periodicamente pelo player de vídeo.
| Campo | Tipo | Descrição |
|---|---|---|
session | ForeignKey → MediaPlaybackSession(CASCADE) | Sessão correspondente |
media | ForeignKey → Media(CASCADE) | Mídia |
current_time | FloatField | Posição atual do player em segundos |
watch_time_delta | FloatField | Tempo assistido desde o último ping |
playback_rate | FloatField(default=1.0) | Velocidade de reprodução |
quality | CharField(20) | Qualidade HLS (1080p, 720p, auto) |
rebuffer_events | IntegerField(default=0) | Quantidade de travamentos detectados |
|-------|------|-----------| | run_id | UUIDField(unique, default=uuid4) | Chave de correlação com sync_log do catalog | | status | CharField(choices: running/success/failed/undone) | Estado do run | | triggered_by | CharField(blank, default="manual") | Quem/o que disparou o run | | stats | JSONField(default=dict) | Contagens por entidade + skipped (plans/faqs, sem modelo Django equivalente) | | error | TextField(blank) | Mensagem de erro, se status=failed | | finished_at | DateTimeField(nullable) | Fim do run |
4.37 Paywall (apps.payments)
Contêiner da oferta de cobrança vinculada a um Space ou via GenericForeignKey a outros alvos (AccessGroup, etc.).
| Campo | Tipo | Descrição |
|---|---|---|
name | CharField(255, blank) | Nome descritivo da oferta |
description | TextField(blank) | Descrição do Paywall |
space | OneToOneField → Space(CASCADE, nullable) | Espaço monetizado |
content_type | ForeignKey → ContentType(CASCADE, nullable) | Alvo dinâmico (GenericFK) |
object_id | PositiveIntegerField(nullable) | ID do alvo dinâmico |
is_active | BooleanField(default=True) | Status do Paywall |
4.38 PaywallPrice (apps.payments)
Preço e variante de cobrança (moeda, ciclo) associada a um Paywall.
| Campo | Tipo | Descrição |
|---|---|---|
paywall | ForeignKey → Paywall(CASCADE) | Paywall pertencente |
price_cents | PositiveIntegerField | Preço em centavos |
currency | CharField(3, default="BRL") | Moeda (BRL, USD, EUR) |
payment_type | CharField(20, default="one_time") | one_time ou subscription |
billing_cycle | CharField(20, nullable) | monthly, quarterly, semestral, yearly |
is_default | BooleanField(default=False) | Indica se é a variante padrão por moeda |
is_active | BooleanField(default=True) | Status do preço |
4.39 Payment (apps.payments)
Registro de transação e status do checkout.
| Campo | Tipo | Descrição |
|---|---|---|
paywall | ForeignKey → Paywall(CASCADE) | Paywall cobrado |
price | ForeignKey → PaywallPrice(CASCADE, nullable) | Variante de preço selecionada |
membership | ForeignKey → Membership(CASCADE, nullable) | Comprador autenticado (se houver) |
guest_email | EmailField(nullable) | E-mail de comprador convidado/não-autenticado |
guest_name | CharField(255, nullable) | Nome do comprador convidado |
status | CharField(20, default="pending") | pending, completed, failed, refunded |
gateway | CharField(20, default="fake") | fake, asaas, etc. |
payment_method | CharField(20, nullable) | pix, card, boleto |
gateway_payment_id | CharField(255, nullable) | ID no gateway |
gateway_customer_id | CharField(255, nullable) | ID do cliente no gateway |
subscription_id | CharField(255, nullable) | ID da assinatura no gateway |
completed_at | DateTimeField(nullable) | Data de aprovação |
4.40 ProductItem (apps.spaces)
Um arquivo digital dentro de um Space digital_product (o "pacote"). Space.access_level + Paywall/Payment (mesmo mecanismo do Course) controlam o acesso ao pacote inteiro; is_free_preview libera um item (e seus vídeos) individualmente mesmo sem compra — mesma ideia de Lesson.free_lesson.
| Campo | Tipo | Descrição |
|---|---|---|
space | ForeignKey → Space(CASCADE) | Apenas em spaces digital_product |
community | ForeignKey → Community(CASCADE) | |
title | CharField(200) | Título do item |
description | TextField(blank) | Descrição |
file | FileField | Arquivo principal (tablatura/preset/midi/sample pack) |
file_type | CharField(20, default="other") | tablature, preset, midi, sample_pack, other |
tuning | CharField(50, blank) | Afinação (tabs) — ex. Standard, Drop D |
difficulty | CharField(20, blank) | beginner, intermediate, advanced |
instrument | CharField(20, blank) | guitar, bass, keys, drums, other |
key | CharField(20, blank) | Tom musical (tabs) |
daw | CharField(50, blank) | DAW alvo (presets) — ex. Ableton, Logic |
plugin | CharField(100, blank) | Plugin alvo (presets) |
genre | CharField(50, blank) | Gênero (presets/samples) |
is_free_preview | BooleanField(default=False) | Libera item + vídeos sem compra do pacote |
order | PositiveIntegerField(default=0) | Ordem de exibição |
Vídeos do item: não é um campo/model próprio — são linhas Media anexadas via o mesmo GFK polimórfico de Post/Comment/Lesson (apps.media.models.MEDIA_ATTACHABLE_MODELS), com content_type/object_id apontando pro ProductItem. Media.title é o label livre do papel do vídeo ("Vídeo de Execução", "Vídeo da Aula", "Vídeo Lento", "Vídeo Rápido", ou qualquer outro — sem enum fixo, não precisa migration pra adicionar um tipo novo). Anexo feito via POST /api/v1/product-items/{item_id}/media/{media_id}/attach/ (ProductItemMediaAttachView, apps.media.views), restrito a admin/moderador.
Endpoints headless: GET /api/headless/v1/products/{package_id}/items (lista com metadata + flag locked, sempre acessível — permite preview do pacote antes da compra) e GET .../items/{item_id} (detalhe completo, ou stub bloqueado se locked=true). Ambos em apps.headless.views (ProductPackageItemsView, ProductPackageItemDetailView).
"Minha Biblioteca" (GET /api/headless/v1/library, LibraryView): agregado cross-space — todo Space course/digital_product a que o membership atual tenha acesso (Space.objects.accessible_to(membership), o mesmo gate usado em todo o resto; nenhuma consulta a Payment é necessária, já que uma compra concluída apenas adiciona o membership ao AccessGroup do space). Cada item vem com progress_percent (curso) ou items_count (curso: nº de aulas; produto: nº de ProductItems). Consumido pela rota /library no SPA (frontend/apps/web/src/routes/library.tsx).
4.41 FileDownloadEvent (apps.downloads)
Log de um download de arquivo (ADR 0016). Um evento por download, com GenericForeignKey para o alvo — DriveFile (Drive), Media (anexo de aula) ou ProductItem (produto digital) — num único modelo. Download é evento discreto (diferente do playback de vídeo, que exige sessão+heartbeats): sem contador denormalizado no dono, derivável por agregação (COUNT/TruncDay).
| Campo | Tipo | Descrição |
|---|---|---|
community | ForeignKey → Community(CASCADE) | related_name: download_events |
content_type / object_id / target | GFK | Alvo do download: DriveFile, Media ou ProductItem |
membership | ForeignKey → Membership(SET_NULL, nullable) | Baixador; nulo para visitantes anônimos (aulas grátis) |
ip_address | GenericIPAddressField(nullable) | IP do cliente (proxy-safe, via apps.utils.middleware.get_client_ip) |
user_agent | TextField(blank) | User-Agent (truncado a 1000 chars) |
referrer | CharField(2048, blank) | Referer HTTP (truncado; CharField de propósito, referrers malformados não podem falhar o tracking) |
mime_type | CharField(255, blank) | Snapshot MIME no momento do download |
file_size | PositiveBigIntegerField(default=0) | Snapshot de bytes |
Registro (transparente, nos pontos onde o backend entrega o arquivo): DriveFileViewSet.download (GET /api/v1/files/{uid}/download/), LessonAttachmentDownloadView (anexos de aula, antes do watermark/redirect) e ProductItemFileView (GET /api/v1/product-items/{item_id}/file/). O MIME é inferido de target.file quando não informado (apps.downloads.services.record_download).
Indexes: (content_type, object_id, -created_at), (community, -created_at). Ordering: -created_at.
4.42 Media (apps.media)
O objeto de vídeo/áudio/imagem no pipeline de processamento assíncrono (Celery, ADR 0004). Anexável a Post/Comment/Lesson/ProductItem/Space via GenericForeignKey ("compor depois anexar" — space/content_type/object_id ficam nulos até o anexo acontecer). Também serve Standalone Media (Studio/apps/stream): community/uploader nulos, user direto, sem GFK.
Identidade, status e artefatos:
| Campo | Tipo | Descrição |
|---|---|---|
media_type | CharField (choices, nullable) | video|audio|image; nulo até import_media_from_url_task detectar (upload direto já vem setado) |
status | CharField (choices) | created→importing→uploaded→transcoding→extracting_audio→transcribing→diarizing(cond.)→summarizing→ready|failed |
failed_stage | CharField (choices, blank) | Snapshot do status no momento da falha (distinto do status ao vivo, usado por MediaReprocessView) |
error_message | TextField(blank) | Erro da última falha |
role | CharField (choices, blank) | Slot nomeado sales_video|demo_video — vídeo de curso a nível de Space (Space.is_accessible_to gate) |
visibility | CharField (choices) | public|unlisted|private (Standalone Media) |
file | FileField | Original enviado |
thumbnail / thumbnail_small | FileField(nullable) | Variantes WebP 1200px/256px (imagem) ou poster de vídeo |
hls_master_playlist | FileField(nullable, max_length=500) | Playlist HLS master; hls_video_url (property) trata URL externa (import via /media/import-hls/) sem passar pelo storage |
renditions | JSONField(list) | [{quality, width, height, bitrate_kbps, playlist_path}, ...] — ladder HLS efetivamente gerado |
extracted_audio | FileField(nullable) | Trilha de áudio extraída (extract_audio_task), reusada pela transcrição |
animated_preview / sprite_vtt | FileField(nullable) | Preview WebP animado (3s) / spritesheet+VTT pra scrubbing |
duration_seconds / width / height | Float / Int / Int (nullable) | Probados via ffprobe |
waveform_peaks | JSONField(list) | Picos de amplitude normalizados (0-1), pra UI de waveform (voice comments) |
Transcrição & IA (resumo/chapters/highlights):
| Campo | Tipo | Descrição |
|---|---|---|
transcript_text | TextField(blank) | Texto plano (com label de speaker se diarizado) |
transcript_raw | JSONField(nullable) | Resposta bruta do provider (Deepgram) — auditoria/reprocessamento |
transcript_json | JSONField(nullable) | Schema agnóstico de provider (apps.media.transcript_schema) — {words, utterances, ...}, o que todo consumidor downstream (captions, structuring, highlights, dublagem) deveria ler |
transcript_srt / transcript_vtt | FileField(nullable) | Legendas geradas (apps.media.services.captions) |
summary | TextField(blank) | Resumo gerado por LLM (apps.media.services.summarization) |
chapters | JSONField(list) | [{start, end, title}, ...] — marcadores de navegação, não confundir com VideoHighlight (4.43) |
highlights | JSONField(list) | [{start, end, label}, ...] — marcadores de destaque, mesma origem/ressalva de chapters |
highlights_status / highlights_error_message | CharField (choices) / TextField | Status da geração dos VideoHighlight (o conjunto de rows), não confundir com o render_status/status de cada row individual — ver ADR 0018 |
Stem Separation (Demucs, opcional, sob demanda — POST /media/<id>/separate-stems/):
| Campo | Tipo | Descrição |
|---|---|---|
stems_status / stems_error_message | CharField (choices) / TextField | Par isolado — uma falha aqui nunca reverte um status já READY pra FAILED |
stems_model | CharField(blank) | Modelo Demucs usado (htdemucs, 4 ou 6 stems — apps.media.demucs_catalog) |
stems | JSONField(dict) | {"vocals": "media-stems/<id>/vocals.wav", ...} |
Dubbing (tradução + TTS, opcional, sob demanda — POST /media/<id>/dub/):
| Campo | Tipo | Descrição |
|---|---|---|
dubbing_status / dubbing_error_message | CharField (choices) / TextField | Par isolado, mesma razão de stems_status. Global, não por-idioma — só um dub roda por vez por media |
dubs | JSONField(dict) | {"en": "media-dubs/<id>/en.mp4", ...} — um entry por idioma já dublado, acumulativo |
dubbing_segments_total / dubbing_segments_done | PositiveIntegerField | Progresso do dub em andamento; done incrementado via F() (segments rodam em paralelo num chord) |
Indexes: (content_type, object_id), (community, status), (space, -created_at). Ordering: -created_at.
Pipeline (ADR 0004): vídeo roda transcode_video_task → extract_audio_task(reencode=True) → transcribe_media_task → [diarize_transcript_task se Whisper+diarização custom] → summarize_media_task; áudio pula transcode/extract; imagem roda só generate_image_thumbnail_task. stems/dubbing/highlights_generate/highlight_render/highlight_cover_frame são side-features opcionais, nunca parte dessa chain automática — disparadas por endpoint próprio, cada uma com seu(s) par(es) de status isolado(s) (ou via PipelineJob, ADR 0018, para as operações por-VideoHighlight).
4.43 VideoHighlight (apps.media)
Um clipe curto candidato selecionado por IA dentro de um Media de vídeo, individualmente renderizável (ADR 0018). Distinto de Media.chapters/Media.highlights (marcadores de timestamp, nunca renderizados) — ver apps.media.services.highlights.generate_highlights (pipeline de duas passadas: "scout" propõe candidatos amplos sobre a transcrição inteira, "curator" decide quais manter e opcionalmente costura dois candidatos num clipe composto setup+payoff).
| Campo | Tipo | Descrição |
|---|---|---|
media | ForeignKey → Media(CASCADE) | related_name: video_highlights |
title / hook / description | CharField / TextField / TextField | Gerados pela passada "curator"; hook é a legenda dos primeiros ~2s |
start_ms / end_ms | PositiveIntegerField | Bounds gerais (min/max) através de todos os segments, pós-snap |
segments | JSONField (list) | [{"start_ms", "end_ms"}, ...] na ordem de corte (não necessariamente cronológica) — 1 entrada = corte simples, 2+ = clipe composto/costurado |
score | FloatField | Potencial viral, 1-10, da passada "curator" |
reasoning | TextField | Justificativa de 1 frase para o score |
status | CharField (choices) | candidate|approved|rejected — estado de curadoria da IA |
render_status | CharField (choices) | not_requested|processing|ready|failed — independente de status; nunca alterado por um status de curadoria e vice-versa |
render_style | CharField (choices) | raw (implementado) ou vertical_story (fast-follow, não implementado ainda) |
rendered_file | FileField(nullable) | Clipe cortado/concatenado, produzido por render_video_highlight_task |
render_error_message | TextField(blank) | Erro da última tentativa de render |
cover_frame | FileField(nullable) | Thumbnail sugerida — ver select_highlight_cover_frame_task; sem par de campos status/error próprio, rastreado via PipelineJob (4.44) |
Timestamps sempre alinhados a palavra: antes de persistir, start/end de cada segmento são ajustados (_snap_to_word_boundary) para a borda real de palavra mais próxima na transcrição — um corte nunca cai no meio de uma palavra.
Indexes: (media, status), (media, score). Ordering: -score.
4.44 PipelineJob (apps.media)
Rastreador genérico de status de job assíncrono para operações de apps.media abaixo do nível de Media inteiro (hoje: geração de highlights, render de highlight, seleção de cover frame). Aditivo, não substitui os pares de campos de status por-feature já existentes em Media (stems_status/dubbing_status/highlights_status) — ver ADR 0018.
| Campo | Tipo | Descrição |
|---|---|---|
media | ForeignKey → Media(CASCADE) | related_name: pipeline_jobs |
job_type | CharField (choices) | highlights_generate|highlight_render|highlight_cover_frame |
status | CharField (choices) | running|done|failed |
progress | PositiveIntegerField(default=0) | 0-100, best-effort (a maioria dos job_types de hoje não reporta progresso incremental) |
payload | JSONField (dict) | Args de entrada; carrega chaves de escopo como highlight_id quando o job_type não é Media-level |
result | JSONField(nullable) | Saída no sucesso |
error | TextField(blank) | Erro no on_failure da task Celery correspondente |
completed_at | DateTimeField(nullable) | Setado em DONE ou FAILED |
Indexes: (media, job_type, status). Ordering: -created_at.
5. Mapa de Relacionamentos
CustomUser
├──< Membership >── Community
│ ├──< PointTransaction (GenericFK target)
│ ├──< ProfileFieldValue >── ProfileFieldDefinition >── Community
│ ├──< AccessGroupMembership >── AccessGroup >── Community
│ ├──< MCPApiKey
│ ├──< Reaction (GenericFK target)
│ ├──< RSVP >── Event >── Space >── SpaceGroup >── Community
│ ├──< Report (reporter, resolved_by)
│ ├──< SpaceModerator >── Space
│ ├──< Post (author)
│ ├──< Bookmark >── Post
│ ├──< PostFollower >── Post
│ ├──< Comment (author)
│ ├──< Image (author)
│ ├──< ChatMessage (author)
│ ├──< ChatRoomMember >── ChatRoom
│ ├──< ChatRoomMessage >── ChatRoom
│ ├──< DMMessage (sender) >── DMThread (membership_a, membership_b)
│ └──< LessonProgress >── Lesson >── Module >── Space
│
Community
├──< Space >── SpaceGroup
│ ├──< Post --< Comment (self-referential parent para replies)
│ ├──< Post --< Bookmark
│ ├──< Post --< PostFollower
│ ├──< Event --< RSVP
│ ├──< Image
│ ├──< ChatMessage
│ ├──< Module --< Lesson --< LessonProgress
│ ├──< Tag (M2M com Post)
│ ├──< AccessGroup (M2M)
│ └──< SpaceModerator
│
├──< APIKey
├──< AccessGroup --< AccessGroupMembership -- Membership
├──< ProfileFieldDefinition --< ProfileFieldValue
├──< ChatRoom --< ChatRoomMember -- Membership
│ └──< ChatRoomMessage -- Membership (sender)
└──< Bookmark -- PostGeneric Foreign Keys (Alvos Polimórficos)
| Modelo | Campo | Alvos Possíveis |
|---|---|---|
Reaction | target | Post, Comment, Image |
Report | target | Post, Comment, Image, ChatMessage |
PointTransaction | target | Post, Comment, Lesson |
FileDownloadEvent | target | DriveFile, Media (anexo de aula), ProductItem |
6. Sistema de Permissões
Matriz de Permissões por Role e Recurso
| Categoria | Ação / Recurso | Admin (Role.ADMIN) | Moderador (Role.MODERATOR) | Space Moderator (SpaceModerator) | Membro Comum (Role.MEMBER) | DRF Permission Class / Rule |
|---|---|---|---|---|---|---|
| Spaces & Cursos | Criar/Editar/Deletar Spaces e SpaceGroups | ✅ | ✅ | ❌ | ❌ | IsAdminOrModeratorForWrite |
| Criar/Editar/Deletar Módulos, Lessons e Tags | ✅ | ✅ | ❌ | ❌ | IsAdminOrModeratorForWrite | |
| Acesso a Spaces Privados/Secretos sem membership | ✅ | ✅ | ❌ | ❌ | Space.is_accessible_to() | |
Gerenciar SpaceModerator | ✅ | ✅ | ❌ | ❌ | IsAdminOrModeratorForWrite | |
| Posts & Conteúdo | Criar/Editar/Deletar conteúdo próprio | ✅ | ✅ | ✅ | ✅ | IsAuthorOrModerator, IsNotMuted |
| Editar/Deletar posts e comentários de outros | ✅ | ✅ | ✅ (apenas no Space) | ❌ | IsAuthorOrModerator | |
| Fixar (pin) e Trancar (lock) posts | ✅ | ✅ | ✅ (apenas no Space) | ❌ | IsAuthorOrModerator | |
| Comentar em posts trancados (locked) | ✅ | ✅ | ✅ (apenas no Space) | ❌ | IsNotLocked | |
| Acessar e gerenciar Fila de Denúncias (Reports) | ✅ | ✅ | ❌ | ❌ | IsAdminOrModerator | |
| Acesso a Direct Messages (DMs) alheias | ❌ | ❌ | ❌ | ❌ | IsDMParticipant, IsDMSender | |
| Membros & Moderação | Banir/Desbanir/Mutar membros MEMBER | ✅ | ✅ | ❌ | ❌ | CanModerateMembership |
Banir/Mutar/Alterar role de MODERATOR | ✅ | ❌ | ❌ | ❌ | CanModerateMembership | |
Banir/Mutar/Alterar role de ADMIN | ✅ | ❌ | ❌ | ❌ | CanModerateMembership | |
| Ações de moderação em si próprio (self-mute/ban) | ❌ | ❌ | ❌ | ❌ | CanModerateMembership |
apps.communities.permissions
| Permissão | Escopo | Descrição |
|---|---|---|
RequiresCommunity | View | Host header deve resolver para uma comunidade conhecida |
RequiresActiveMembership | View | Request deve ter uma membership ativa |
IsAuthenticatedOrHasAPIKey | View | Auth por usuário ou API key |
CanModerateMembership | Object | Verifica roles admin/moderator para ações de ban/mute |
apps.spaces.permissions
| Permissão | Escopo | Descrição |
|---|---|---|
HasSpaceAccess | Object | Delega para Space.is_accessible_to() |
IsAuthorOrModerator | Object | Autor, admin/moderator, ou SpaceModerator |
IsAdminOrModeratorForWrite | View | Admin/moderator para escritas estruturais |
IsAdminOrModerator | View | Admin/moderator para todos os métodos (fila de moderação) |
IsNotLocked | View | Post não pode estar trancado (para comentários) |
IsNotMuted | View | Membership não pode estar silenciada (para escritas) |
IsDMParticipant | Object | Deve ser participante do DM thread |
IsDMSender | Object | Deve ser o remetente da DM message (sem bypass de mod) |
7. Middlewares
TenantResolutionMiddleware
Arquivo: apps/communities/middleware.py
- Extrai o host do request
- Busca
Communityporslugoucustom_domain - Anexa
request.community - Se autenticado, busca
Membershipe anexarequest.membership(lazy) - Modo debug: suporta header
X-Community-Slugpara override
TenantWebsocketAuthMiddleware
Arquivo: apps/communities/channels_middleware.py
- Resolve comunidade a partir do Host
- Autentica via token de uso único armazenado no Redis (TTL 30s)
- O token é gerado pelo endpoint REST
POST /community/ws-token/
8. APIs
8.1 API REST Interna (api/v1/)
Endpoints DRF com JWT + Session + API Key auth.
Users:
GET/PATCH /api/v1/users/me/— Dados do usuário atual
Communities:
GET /api/v1/community/— Comunidade atual + membershipGET/PATCH /api/v1/community/profile-fields/— Campos de perfil customizadosPOST /api/v1/community/ws-token/— Token para WebSocketGET /api/v1/community/leaderboard/— Ranking por pontosPOST /api/v1/community/members/<id>/ban/— Banir membroPOST /api/v1/community/members/<id>/unban/— Desbanir membroPOST /api/v1/community/members/<id>/mute/— Silenciar membroPOST /api/v1/community/members/<id>/unmute/— Dessilenciar membro
Onboarding & Provisionamento B2B (autenticação JWT, escopo global — fora do contexto de tenant):
POST /api/v1/communities/create/— Cria uma nova comunidade (requeruser.can_create_community). Aceita campos de PWA (pwa_short_name), branding (primary_color), layout de autenticação (auth_layout), controle de acesso (allow_public_signup,email_verification_mode). O owner é automaticamente criado comoMembershipcom roleADMINe statusACTIVEviapost_savesignal.POST /api/v1/communities/invites/— Convite em lote de membros para uma comunidade. Aceita lista de e-mails erole(member|moderator). Para cada e-mail: criaMembershipcom statusINVITED(ou reutiliza existente viaget_or_create), e dispara e-mail HTML de convite viasend_community_invitation_email(templateemails/community_invitation.html). Requer que o requester sejaADMINda comunidade.GET /api/v1/users/me/— Estendido comcan_create_community,has_owned_communityeowned_communities(lista de{id, name, slug}) para o app de onboarding saber o estado do criador.
Spaces:
- CRUD via DRF Router:
space-groups/,spaces/,tags/
Posts:
GET/POST /api/v1/spaces/<id>/posts/GET/PUT/PATCH/DELETE /api/v1/posts/<id>/POST /api/v1/posts/<id>/react/GET/POST /api/v1/posts/<id>/comments/
Comentários:
GET/PUT/PATCH/DELETE /api/v1/comments/<id>/POST /api/v1/comments/<id>/react/
Eventos:
GET/POST /api/v1/spaces/<id>/events/GET/PUT/PATCH/DELETE /api/v1/events/<id>/POST/DELETE /api/v1/events/<id>/rsvp/
Imagens:
GET/POST /api/v1/spaces/<id>/images/GET/PUT/PATCH/DELETE /api/v1/images/<id>/POST /api/v1/images/<id>/react/
Cursos:
GET/POST /api/v1/spaces/<id>/modules/GET/PUT/PATCH/DELETE /api/v1/modules/<id>/GET/POST /api/v1/modules/<id>/lessons/GET/PUT/PATCH/DELETE /api/v1/lessons/<id>/POST/DELETE /api/v1/lessons/<id>/progress/
Chat:
GET/POST /api/v1/spaces/<id>/chat-messages/GET/PUT/PATCH/DELETE /api/v1/chat-messages/<id>/
DMs:
GET/POST /api/v1/dm-threads/GET/POST /api/v1/dm-threads/<id>/messages/GET/PUT/PATCH/DELETE /api/v1/dm-messages/<id>/
Moderação:
POST /api/v1/posts/<id>/report/POST /api/v1/comments/<id>/report/POST /api/v1/images/<id>/report/POST /api/v1/chat-messages/<id>/report/GET /api/v1/moderation/reports/GET/PATCH /api/v1/moderation/reports/<id>/
Downloads (ADR 0016):
GET /api/v1/downloads/analytics/?object_type=drive|media|product&object_id=<uid>— Sumário de downloads de um alvo:total_downloads,downloads_30d,unique_downloaders,average_per_day_30d,timeline(TruncDay),top_downloaders,last_downloads.object_idé sempre ouid(UUID). Admin/moderador (IsAdminOrModerator, mesmo gate doMediaAnalyticsSummaryView).GET /api/v1/community/admin/analytics/— dashboard admin; inclui a seçãofiles(total_downloads,top_filescom label/tipo/id/downloads resolvidos por modelo via GFK,timeline30d).- Registro transparente (sem endpoint novo):
GET /api/v1/files/{uid}/download/(Drive),GET /api/headless/v1/courses/{course}/lessons/{lesson}/attachments/{media}/download(anexo de aula) eGET /api/v1/product-items/{item_id}/file/(produto digital) criamFileDownloadEventantes de entregar o arquivo. - Vídeo:
POST /api/v1/media/{uid}/heartbeat/continua permissivo (ingestão);GET /api/v1/media/{uid}/analytics/passou a exigir admin/moderador (ADR 0016).
Media — Pipeline Central (upload, transcrição, stems, dublagem — ADR 0004):
GET/POST /api/v1/media/— lista a fila de "compor" do uploader atual (media ainda não anexada) / cria upload direto (multipart) e disparaenqueue_media_processing.POST /api/v1/media/import-url/— cria Media emIMPORTINGa partir de uma URL (YouTube/Vimeo viayt-dlp, ou download direto), disparaimport_media_from_url_task.POST /api/v1/media/import-hls/— registra um vídeo HLS já hospedado externamente, sem download/transcode; Media nasceREADY.GET /api/v1/media/<id>/— poll de status/resultado (gated por_media_accessible_to: uploader sempre, ou membro com acesso ao Space uma vez anexado).GET /api/v1/media/<id>/public-config/— endpoint público (sem auth) pro player standalone/embed.POST /api/v1/media/<id>/reprocess/— reinicia o pipeline do zero (ou re-dispara o import, sefailed_stage=IMPORTING); só sestatus=FAILED.POST /api/v1/media/<id>/extract-audio/— extração de áudio standalone ({reencode, bitrate_kbps, sample_rate, channels}), desacoplada do resto do pipeline.POST /api/v1/media/<id>/separate-stems/— Stem Separation (Demucs) sob demanda, filastems(worker-stems). Rejeita se jáPROCESSINGou semedia_type=image.POST /api/v1/media/<id>/dub/— Dublagem sob demanda ({target_language, translation_provider?, tts_engine?}), filadubbing(worker-dubbing). Requer transcrição já feita; rejeita se jáPROCESSING.POST /api/v1/{posts,comments,lessons,product-items}/<id>/media/<mediaId>/attach/,POST /api/v1/courses/<id>/{sales-video,demo-video}/<mediaId>/attach/— anexa um Media já enviado ("compor depois anexar"); os dois últimos (curso) e o de produto digital são admin/moderador-only.- Standalone (Studio,
apps/stream):GET/POST /api/v1/videos/(coleção do usuário),GET /api/v1/videos/<id>/(poll),POST /api/v1/videos/<id>/fetch/(ingestão por URL, step 2 do fluxo Bunny-like),POST /api/v1/videos/probe/(probe de URL externa antes de importar),POST /api/v1/videos/tus/complete/(webhook dotusd-media, autenticado por secret compartilhado, não por sessão).
Media — Highlights com IA (ADR 0018):
POST /api/v1/media/<id>/highlights/generate/— disparagenerate_video_highlights_task({force: bool}). 400 se sem transcrição ou já em progresso. Apenas o uploader.GET /api/v1/media/<id>/highlights/— listaVideoHighlightdo media, ordenado por-score.GET/PATCH /api/v1/media/highlights/<id>/— detalhe (poll de status) / atualização destatus(candidate\|approved\|rejected).POST /api/v1/media/highlights/<id>/render/— dispararender_video_highlight_task({style}, sórawaceito na v1). Apenas o uploader.POST /api/v1/media/highlights/<id>/cover-frame/— disparaselect_highlight_cover_frame_task({strategy: exact|thumbnail|ai}, defaultai). Apenas o uploader.GET /api/v1/media/<id>/pipeline-jobs/— listaPipelineJobdo media (todo job de highlights/render/cover-frame disparado, mais recente primeiro).- Todos gated por
_media_accessible_to(leitura) / uploader-only (escrita), mesmo padrão deMediaSeparateStemsView/MediaDubView.
8.2 API Headless (compatível com Circle.so)
URL base: api/headless/v1/
Endpoints REST retornando JSON com shape compatível com Circle.so. 52 endpoints no total, com 51 implementados.
Auth/User (6):
community_member— Dados do membro atualprofile— Dados do perfil com campos customizadoscommunity_members— Listagem de membros buscávelcommunity_members/<id>/public_profile— Perfil público (posts, comments, followers)community_members/<id>/spaces— Spaces do membrosearch/community_members— Busca por membros
Spaces (8):
spaces/spaces/home— Listagem de spaces e home feedspaces/<id>/posts— Posts de um spacespaces/<id>/posts/<id>— Detalhe de um postspaces/<id>/join/leave— Entrar/sair de spacespaces/<id>/topics— Tags do spacespaces/<id>/bookmarks— Bookmarks do space
Posts/Comments (6):
posts— Criar postposts/<id>/comments— Listar/criar comentáriosposts/<id>/comments/<id>— Editar/deletar comentárioposts/<id>/user_likes— Curtir/descurtir postposts/<id>/post_followers— Seguir/deixar de seguir postcomments/<id>/user_likes/replies— Curtir comentário, criar reply
Notifications (7):
notifications— Listar notificaçõesnotifications/new_notifications_count— Contagem não lidasnotifications/mark_all_as_read— Marcar todas como lidasnotifications/<id>/mark_as_read/archive— Ações individuaisspace_notification_details— Detalhes por spacenotification_preferences/<medium>— Preferências por canalnotification_preferences/<medium>/spaces— Preferências por space
Bookmarks (2):
bookmarks— Listar/criar bookmarksbookmarks/<postId>— Deletar bookmark
Events (4):
community_events— Eventos da comunidadeevents/<id>/event_attendees— Listar/confirmar/cancelar presençaspaces/<id>/events/<id>/recurring_events— Eventos recorrentesspaces/<id>/events/<id>/recurring_events/rsvp— RSVP recorrente
Reactions (1):
reactions— Criar reação em Post/Comment/Image
Chat/Messages (8):
messages— Listar/criar salas de chatmessages/<uuid>/chat_room_messages— Listar/enviar mensagensmessages/<uuid>/mark_all_as_read— Marcar como lidomessages/unread_chat_rooms— Salas não lidaschat_threads/chat_threads/<id>— Threads DM
Courses (3):
courses/<id>/sections— Módulos do cursocourses/<id>/lessons/<id>— Detalhe da aula; incluivideo_media_id(uid doMediado vídeo principal) para o player apontar a telemetria de playback (ADR 0016;nullno stublocked)courses/<id>/lessons/<id>/progress— Progresso da aula
Outros (5):
advanced_search— Busca full-text em postsinvitation_links/<token>/join— Aceitar convitepage_profile_fields— Campos de perfil da páginacommunity_links— Links da comunidade (premium, stub)community_branding— PATCH: define/remove logo_light/logo_dark/pwa_icon/pwa_icon_maskable (via Media id, mesmo fluxo de upload-then-attach do avatar/banner) e pwa_short_namereactions— Criar reação
Micro-apps (2):
community/micro-apps— GET: apps habilitados com config merged (defaults + overrides, chaves desconhecidas descartadas). Staff com?include_disabled=truerecebe o catálogo inteiro comis_enabled.community/micro-apps/<app_id>— PATCH (admins/moderadores): toggleis_enabled,ordere merge deconfigvalidado antes do save (400{"error", "details"}; PATCH inválido não cria row). Catalogo emapps/communities/micro_apps.py; rows emCommunityMicroApp. Micro-apps não são conteúdo de Space → fora do contrato visible/accessible_to (qualquer membership ativa lê). Ver ADR-0017.
Nota:
invitation_links(listar/criar convites) einvitation_links/<token>(deletar) são restritos a admins/moderadores (IsAdminOrModerator) — convites expõem e-mails de membros e emitem tokens de convite. O fluxo do convidado (invitation_links/<token>/join) continua público para quem possui o token.community_brandingé restrito a admins apenas (não moderadores) — ver ADR-0015.
8.2.1 Contrato de acesso — boas práticas obrigatórias (sempre seguir)
Todo endpoint headless que retorna conteúdo de um Space deve passar por um dos três gates canônicos. Isso não é opcional — um get_object_or_404 de modelo de conteúdo sem gate é bug e é rejeitado pela guarda estrutural em CI:
| Gate | Uso |
|---|---|
_resolve_accessible_space(membership, space_id) | Endpoint de um único space (404 se invisível, 403 se inacessível) |
<Modelo>.objects.accessible_to(membership) | Queries cross-space (feeds, agregados) |
visible_content_qs(membership, model) + resolve_accessible_object(membership, qs, pk) (em apps/spaces/permissions.py) | Lookup de um objeto de conteúdo (Post/Comment/Event/Lesson/Quiz/Announcement), inclusive em rotas de mutação (comentários, reações, restaurações, denúncias, progresso) |
Regras:
visible_toé só para navegação (sidebar):SpaceJoinView,SpaceLeaveView,SpaceMarkVisitedView. Nunca usar para retornar conteúdo — um membro pode ver um space Private sem poder ler os posts dele.Autorização em cima do gate reusa as permission classes do
/v1:IsAuthorOrModerator,IsAdminOrModerator,IsNotMuted,IsNotLocked— não reimplementar checagens frouxas.Locked/secret → 404, nunca 200 com dados.
CourseLessonDetailView.geté a única exceção permitida (serve o stublocked=Truecomsections: []), e está na allowlist explícita do teste.Exemplos corretos / incorretos:
python# ✅ Gate de space único space = _resolve_accessible_space(request.membership, space_id) # ✅ Gate cross-space Event.objects.accessible_to(request.membership) # ✅ Gate por objeto (visible_content_qs em apps/spaces/permissions.py) lesson = get_object_or_404(visible_content_qs(membership, Lesson).filter(space_id=course_id), pk=lesson_id) comment = resolve_accessible_object(membership, visible_content_qs(membership, Comment), comment_id) # ❌ Errado — só checa visibilidade de sidebar, não acesso space = get_object_or_404(Space.objects.visible_to(membership), pk=space_id) # ❌ Errado — nenhum gate; leitura cross-community/Secret post = get_object_or_404(Post, pk=post_id)CI faz cumprir a regra:
StructuralAccessGuardTests(emapps/headless/tests/test_headless_access_regressions.py) varre o código-fonte deheadless/views.pye falha se umget_object_or_404de modelo de conteúdo (Post/Comment/Event/Lesson/Quiz/Announcement) aparecer num handler sem gate. Testes comportamentais emapps/headless/tests/test_visibility_contracts.pycobrem o mesmo contrato com outsider/insider.
8.3 Autenticação JWT
Endpoint: POST /api/v1/auth/token/
{
"email": "user@example.com",
"password": "senha123"
}Resposta:
{
"access": "eyJ...",
"refresh": "eyJ..."
}- Access token: 30 minutos
- Refresh token: 14 dias com rotação
- Blacklist habilitado para logout
8.4 Servidor MCP (FastMCP)
URL base / Endpoint: /mcp (redirecionamento automático de /mcp para /mcp/ via HTTP 307 em ASGI)
O sistema integra o protocolo Model Context Protocol (MCP) via FastMCP para permitir que assistentes de IA e agentes externos realizem consultas e operações de gerenciamento na plataforma.
Arquitetura ASGI e Roteamento
- A aplicação FastMCP (
mcp.http_app) é montada no arquivoproject/asgi.pyatravés do Starlette. - O Starlette roteia requisições em
/mcppara a aplicação HTTP stateless do FastMCP, enquanto repassa as demais requisições aoProtocolTypeRouter(Django + Channels), preservando o suporte a WebSockets.
Autenticação (apps.mcp_server.auth)
- Autenticação via header
Authorization: Bearer <mcp_api_key>. - O hash SHA-256 da chave recebida é comparado com os registros do modelo
MCPApiKey(revoked_at__isnull=True). - A
Membershipassociada deve terstatus="active". - O contexto da requisição fornece automaticamente:
user: Usuário associado à membershipcommunity: Tenant da comunidademembership: Objeto de adesão (com role e permissões do membro)
Tools Disponíveis (apps/mcp_server/tools/)
Ferramentas registradas via @mcp.tool() para uso pelos agentes de IA:
| Módulo | Descrição das Ferramentas |
|---|---|
tools.communities | Consulta detalhes da comunidade e contagem de membros |
tools.spaces | Listagem e gestão de espaços e grupos de espaços |
tools.posts | Listagem, criação, busca e moderação de posts e comentários |
tools.users | Consulta de perfis de membros e informações de usuários |
9. WebSockets
URLs
| Padrão | Consumer | Uso |
|---|---|---|
ws/chat/spaces/<space_id>/ | ChatConsumer | Chat ao vivo em spaces |
ws/dm/threads/<thread_id>/ | DMConsumer | Mensagens diretas |
Autenticação WebSocket
- Usuário faz
POST /community/ws-token/via REST - Token de uso único é armazenado no Redis com TTL de 30 segundos
- WebSocket handshake envia o token
TenantWebsocketAuthMiddlewareconsome o token e autentica
Broadcasting
Função: apps.spaces.broadcasting.broadcast(group_name, payload)
Ponte síncrona-to-channel-layer para推送 atualizações em tempo real das views DRF para os consumers WebSocket.
10. Gamificação
Sistema de Pontos
Arquivo: apps/communities/gamification.py
| Ação | Pontos |
|---|---|
post_created | 10 |
comment_created | 5 |
lesson_completed | 15 |
Níveis
| Nível | Pontos Necessários |
|---|---|
| 1 | 0 |
| 2 | 100 |
| 3 | 300 |
| 4 | 700 |
| 5 | 1500 |
Regras
award_points()é idempotente (get_or_create na constraint única)- Pontos são concedidos automaticamente ao completar uma lesson (via
LessonProgressView) - Transações são validadas cross-tenant em
clean()
11. Sinais (Signals)
apps.communities/signals.py
| Sinal | Ação |
|---|---|
post_save(Community) | Auto-cria Membership admin para o owner da comunidade |
apps.users/signals.py
| Sinal | Ação |
|---|---|
user_signed_up | Notifica admins de novos registros |
email_confirmed | Define email confirmado como primário |
pre_save(CustomUser) | Deleta arquivo de avatar antigo quando substituído |
post_delete(CustomUser) | Deleta arquivo de avatar ao deletar usuário |
12. Configurações
Arquivo principal: project/settings.py
Variáveis de Ambiente (via django-environ)
| Variável | Padrão | Descrição |
|---|---|---|
| Core & Segurança | ||
SECRET_KEY | django-insecure-... | Chave secreta de criptografia do Django |
DEBUG | True | Ativa o modo de depuração e profiler |
ALLOWED_HOSTS | * | Domínios permitidos para requisições |
ROOT_DOMAIN | localhost | Domínio apex para resolução de subdomínios multi-tenant |
FRONTEND_URL | http://localhost:3000 | URL da aplicação frontend Next.js/Web |
USE_HTTPS_IN_ABSOLUTE_URLS | False | Força esquemas HTTPS em links absolutos gerados |
CSRF_TRUSTED_ORIGINS | localhost:3000/8000/8080 (dev) | Origins com permissão de CSRF; deve incluir a origin do app checkout (frontend/apps/checkout, ver ADR 0014) em cada ambiente |
| Banco de Dados & Cache | ||
DATABASE_URL | PostgreSQL local | URL de conexão PostgreSQL (Postgres local ou Neon) |
CATALOG_DATABASE_URL | None | URL do Postgres do serviço externo catalog |
DJANGO_DATABASE_CONN_MAX_AGE | 60 | Tempo máximo de reutilização de conexões DB (segundos) |
REDIS_URL | redis://127.0.0.1:6379 | URL do Redis (usado por Cache, Celery e WebSockets) |
| Armazenamento (S3/MinIO/R2) | ||
AWS_STORAGE_BUCKET_NAME | project-media | Nome do bucket S3/MinIO para mídias |
AWS_S3_ENDPOINT_URL | http://127.0.0.1:9000 | Endpoint do MinIO/Cloudflare R2/S3 |
AWS_S3_REGION_NAME | us-east-1 | Região do serviço S3 |
AWS_ACCESS_KEY_ID | minioadmin | Access Key do S3/MinIO |
AWS_SECRET_ACCESS_KEY | minioadmin | Secret Key do S3/MinIO |
DRIVE_TUS_WEBHOOK_SECRET | change-me-drive-tus | Chave secreta compartilhada com o servidor tusd |
DRIVE_TUS_PUBLIC_URL | http://localhost:1080 | URL pública do servidor de upload resumível tusd |
| Transcrição & Mídia (Deepgram & FFmpeg) | ||
FFMPEG_BINARY | ffmpeg | Caminho do binário do FFmpeg no sistema |
FFPROBE_BINARY | ffprobe | Caminho do binário do FFprobe no sistema |
FFMPEG_TIMEOUT_SECONDS | 1800 | Timeout máximo para execuções do FFmpeg |
DEEPGRAM_API_KEY | "" | Chave de API da Deepgram para transcrição de áudio |
DEEPGRAM_MODEL | nova-3 | Modelo de transcrição do Deepgram (nova-3, whisper-large, etc.) |
DEEPGRAM_DIARIZE_MODEL | latest | Modelo de diarização de oradores do Deepgram |
DEEPGRAM_DETECT_LANGUAGE | False | Ativa detecção automática de idioma no Deepgram |
| Resumos por IA & Tradução LLM | ||
AI_SUMMARY_BASE_URL | https://api.openai.com/v1 | URL base da API OpenAI-compatible (OpenAI, Groq, Ollama) |
AI_SUMMARY_API_KEY | "" | Chave de API para a LLM de resumos e traduções |
AI_SUMMARY_MODEL | gpt-4o-mini | Modelo da LLM usado para geração de resumos |
AI_VISION_MODEL | gpt-4o-mini | Modelo LLM vision-capable para seleção de cover frame de highlight (apps.media.services.frame_picker); mesmo endpoint AI_SUMMARY_BASE_URL/AI_SUMMARY_API_KEY |
LIBRETRANSLATE_URL | http://localhost:5000 | URL do servidor de tradução LibreTranslate |
TRANSLATION_PRIMARY_PROVIDER | libretranslate | Provedor primário de tradução (libretranslate, argos, llm) |
| IA Avançada (Stems, Dublagem, Diarização Local) | ||
DEMUCS_BINARY | demucs | Binário do Demucs para separação de faixas de áudio |
DEMUCS_MODEL | htdemucs | Modelo de IA para separação de áudio (htdemucs) |
DEMUCS_DEVICE | cpu | Dispositivo de processamento do Demucs (cpu ou cuda) |
DUBBING_TRANSLATION_PROVIDER | argos | Provedor de tradução do pipeline de dublagem (argos ou llm) |
DUBBING_TTS_ENGINE | piper | Motor de síntese de voz para dublagem (piper) |
PIPER_BINARY | piper | Binário do Piper TTS no sistema |
PIPER_VOICES_DIR | piper-voices | Diretório de modelos de vozes do Piper |
DIARIZATION_ENGINE | resemblyzer | Engine de diarização local (resemblyzer ou pyannote) |
HUGGINGFACE_TOKEN | "" | Token do HuggingFace (necessário para pyannote) |
YTDLP_BINARY | yt-dlp | Binário do yt-dlp para importação de vídeos por URL |
| Live Rooms & Streaming | ||
VIDEO_PROVIDER | livekit | Provedor de vídeo ao vivo para novas salas (livekit ou hms) |
LIVEKIT_URL | http://localhost:7880 | URL do servidor LiveKit self-hosted |
LIVEKIT_API_KEY | devkey | API Key do LiveKit |
LIVEKIT_API_SECRET | secret | API Secret do LiveKit |
| Pagamentos (Asaas) | ||
ASAAS_ACCESS_TOKEN | "" | Token da API do gateway de pagamento Asaas |
ASAAS_BASE_URL | https://sandbox.asaas.com/api/v3 | Endpoint da API Asaas (Sandbox/Produção) |
ASAAS_WEBHOOK_SECRET | change-me-asaas-webhook | Chave de validação de webhooks do Asaas |
| Error Tracking (Glitchtip / Sentry) | ||
SENTRY_DSN | "" | DSN para envio de exceções (Glitchtip local http://...:8088/1 ou Sentry SaaS) |
SENTRY_TRACES_SAMPLE_RATE | 0.1 | Taxa de amostragem de rastreamento de performance |
GLITCHTIP_PORT | 8088 | Porta do container local do Glitchtip (make glitchtip-start) |
Configurações Chave
| Configuração | Valor |
|---|---|
| JWT Access Token | 30 minutos |
| JWT Refresh Token | 14 dias |
| JWT Rotação | Habilitada |
| JWT Blacklist | Habilitado |
| Throttling (user) | 1000/hora |
| Throttling (API key) | 5000/hora |
| Cache (DEBUG) | DummyCache |
| Cache (produção) | RedisCache |
| Storage | S3 (MinIO-compatible) |
Settings de Produção
Arquivo: project/settings_production.py
DEBUG=False- SSL redirect habilitado
- Cookies seguros
Arquitetura de Workers Celery & Filas
O sistema adota uma segregação de tarefas assíncronas baseada em perfis de carga e filas no Redis (CELERY_TASK_ROUTES), descrita em detalhes no ADR 0005:
| Worker | Filas (-Q) | Tarefas Executadas | Perfil / Requisitos |
|---|---|---|---|
worker-main | default, celery, critical, media-import | transcribe_media_task (API Deepgram), summarize_media_task (API LLM), generate_image_thumbnail_task, generate_video_highlights_task, select_highlight_cover_frame_task, send_broadcast_campaign_task, process_access_group_webhook, send_notification_digests, process_deletion_reminders, execute_account_deletion, notify_space_members_of_post, import_media_from_url_task (yt-dlp) | I/O-bound e chamadas de API externas — todas mesma imagem/perfil de recurso, separadas em filas por prioridade (não por processo), concorrência alta (-c 8) pra um download/highlight lento não segurar os outros |
worker-heavy | heavy | transcode_video_task (FFmpeg HLS Ladder), extract_audio_task, render_video_highlight_task (corte/concat FFmpeg) | CPU-intensive local (conversão de vídeo/áudio) — isolado por perfil de recurso real, não cabe no worker-main |
worker-stems | stems | separate_stems_task (Demucs) | IA/ML heavy (PyTorch) |
worker-dubbing | dubbing | dub_media_task, dub_segment_task, prepare_dub_background_task, finalize_dub_task | IA/ML heavy (Piper TTS + Argos Translate) |
worker-diarization | diarization | diarize_transcript_task (Pyannote) | IA/ML heavy (PyTorch) |
Em produção (docker-compose.prod.yml), worker-stems/worker-dubbing/worker-diarization ficam atrás do Compose profile ml (docker compose --profile ml up -d) — não sobem por padrão porque a imagem app:latest não inclui os extras ML (torch/demucs/argostranslate/piper/pyannote); decidir host/imagem própria pra eles antes de ativar.
worker-light/worker-critical/worker-import existiram como processos/máquinas separados até 2026-08-02 — fundidos em worker-main porque compartilhavam o mesmo perfil de recurso (I/O-bound, mesma imagem); a separação por processo não tinha justificativa de recurso, só de fila/prioridade, então virou 3 máquinas sempre ligadas pagando por isolamento que uma única fila multiplexada já resolve. Ver ADR 0005, seção "Atualização (2026-08-02) parte 2".
13. Commands de Gestão
| Command | App | Descrição |
|---|---|---|
seed | apps.web | Popula o banco com dados de exemplo |
bootstrap_celery_tasks | apps.web | Cria tarefas periódicas no django-celery-beat |
send_test_email | apps.web | Testa configuração de email |
promote_user_to_superuser | apps.users | Promove usuário a superuser |
sync_catalog | apps.spaces | Sincroniza Community/Space (course)/Module/Lesson para o Postgres do serviço catalog em _internal/apps/catalog (infra separada, fora deste app Django — ver _internal/README.md). Upsert por external_id (PK inteira do Django). --dry-run roda os upserts reais e reverte (nada é persistido/logado); --undo <run_id> reverte um run anterior via audit_undo do lado do catalog. Cada run é logado nos dois bancos, ligados pelo mesmo run_id: apps.communities.models.CatalogSyncLog aqui, sync_log no catalog. Plan/FAQ não são sincronizados — não existe modelo Django equivalente ainda (Paywall é por-Space e não tem checkout URL; FAQ não existe). |
14. Endpoints Principais
Autenticação
| Método | Endpoint | Descrição |
|---|---|---|
| POST | /accounts/login/ | Login (allauth) |
| POST | /accounts/signup/ | Registro (allauth) |
| POST | /accounts/logout/ | Logout (allauth) |
| POST | /api/v1/auth/token/ | JWT token |
| POST | /api/v1/auth/token/refresh/ | Refresh JWT |
Usuário
| Método | Endpoint | Descrição |
|---|---|---|
| GET/PATCH | /api/v1/users/me/ | Dados do usuário |
| GET/POST | /users/profile/ | Perfil (template) |
| POST | /users/profile/upload-image/ | Upload avatar |
Comunidade
| Método | Endpoint | Descrição |
|---|---|---|
| GET | /api/v1/community/ | Comunidade atual |
| GET/PATCH | /api/v1/community/profile-fields/ | Campos de perfil |
| POST | /api/v1/community/ws-token/ | Token WebSocket |
| GET | /api/v1/community/leaderboard/ | Ranking |
Spaces e Conteúdo
| Método | Endpoint | Descrição |
|---|---|---|
| GET/POST | /api/v1/spaces/ | Listar/criar spaces |
| GET/POST | /api/v1/spaces/<id>/posts/ | Posts do space |
| GET/POST | /api/v1/spaces/<id>/events/ | Eventos do space |
| GET/POST | /api/v1/spaces/<id>/images/ | Imagens do space |
| GET/POST | /api/v1/spaces/<id>/modules/ | Módulos do curso |
| GET/POST | /api/v1/spaces/<id>/chat-messages/ | Mensagens do chat |
Downloads (ADR 0016)
| Método | Endpoint | Descrição |
|---|---|---|
| GET | /api/v1/downloads/analytics/?object_type=...&object_id=<uid> | Sumário de downloads de um alvo (admin/moderador) |
| GET | /api/v1/files/<uid>/download/ | Download de arquivo do Drive (registra FileDownloadEvent) |
| GET | /api/v1/product-items/<uid>/file/ | Download de produto digital (registra FileDownloadEvent) |
Servidor MCP
| Método | Endpoint | Descrição |
|---|---|---|
| GET/POST/DELETE | /mcp/ | Endpoint do Servidor MCP (FastMCP / SSE / Stateless HTTP) |
Documentação da API
| Endpoint | Descrição |
|---|---|
/api/schema/ | Schema OpenAPI (JSON) |
/api/schema/swagger-ui/ | Swagger UI |
/api/schema/redoc/ | ReDoc |
15. Otimização de Imagens (WebP)
Toda imagem armazenada no backend passa por otimização (resize + conversão WebP) via um único serviço reutilizável: backend/apps/media/services/optimize.py. Pillow é a única dependência (já existia para o pipeline de media).
Serviço (apps/media/services/optimize.py):
optimize_image(file, max_dimension, ...)→ bytes WebP em memória (EXIF-transpose + resize proporcional, nunca upscale; preserva transparência; qualidade 82).optimize_image_content_file(file, max_dimension, ...)→ContentFilenomeado.webppara atribuição direta emFileField(usado por serializers/views).optimize_image_to_path(input_path, output_path, ...)→ variante para o pipeline Celery; lançaImageOptimizationErrorse o arquivo não for imagem decodificável (falha rápido, sem retry inútil).- Fallback defensivo: bytes inválidos com extensão válida retornam o arquivo original em vez de derrubar o request.
Pipeline Media (generate_image_thumbnail_task, apps/media/tasks.py): agora gera duas variantes WebP do original:
Campo do Media | Tamanho | Uso |
|---|---|---|
thumbnail | 1200px | Banners, cover photo, feed |
thumbnail_small | 256px | Avatares, ícones, logos |
Onde cada imagem é otimizada:
| Superfície | Caminho | Detalhe |
|---|---|---|
Galeria Image | apps/spaces/serializers/images.py (ImageSerializer.create/update) | Síncrono, WebP 1200px |
| Avatar (templates Django) | apps/users/views.py::upload_profile_image | Síncrono, WebP 256px |
| Avatar/cover/banner/icon/logo (SPA/headless) | apps/headless/views.py::_pick_media_file | Usa thumbnail_small/thumbnail do Media com fallback pro original (o attach pode correr antes do Celery terminar) |
Atenção: o upload via SPA (avatar/cover/banner/icon/logo) aponta para o arquivo otimizado após a task Celery de thumbnails rodar. Até lá, o campo aponta para o original (fallback) — otimização é best-effort, nunca bloqueia o request. Imagens já existentes não são re-otimizadas retroativamente (só novos uploads).