Skip to content

Documentação Técnica — Backend Django

Clone de Community Platform (estilo Circle.so) Django 6 · Python 3.14 · PostgreSQL · Redis · Celery · Channels


Sumário

  1. Visão Geral da Arquitetura
  2. Stack Tecnológica
  3. Estrutura de Apps
  4. Modelos de Dados
  5. Mapa de Relacionamentos
  6. Sistema de Permissões
  7. Middlewares
  8. APIs
  9. WebSockets
  10. Gamificação
  11. Sinais (Signals)
  12. Configurações
  13. Commands de Gestão
  14. 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 → Database

Camadas de Acesso

O sistema implementa três camadas de acesso independentes:

CamadaPerguntaImplementaçã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

ComponenteTecnologiaFunção
FrameworkDjango 6MVC backend
LinguagemPython 3.14Runtime
BancoPostgreSQLDados persistentes
Cache/BrokerRedisCache, sessões, Celery broker, WebSocket tokens
Autenticaçãodjango-allauthLogin, registro, email verification
API RESTDjango Rest Framework + JWTEndpoints JSON
Protocolo MCPFastMCPServidor MCP para integração com assistentes de IA
TarefasCelery + django-celery-beatJobs em background com arquitetura de workers segregados por perfis de carga (Light, Heavy, Import, Stems, Dubbing, Diarization)
WebSocketsDjango ChannelsChat ao vivo e DMs
FrontendTailwind v4 + DaisyUI + HTMX + Alpine.jsInterface
BundlerVite via django-viteAssets JS/CSS
ArmazenamentoS3 (MinIO-compatible)Upload de imagens
OpenAPIdrf-spectacularSchema + 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 tags

Mapa de Responsabilidades

AppResponsabilidade
utilsBaseModel abstrato estendido por todos os modelos concretos
usersCustomUser com avatar, display_name, email verification
communitiesTenancy (Community), adesão (Membership), gamificação, API keys, grupos de acesso, campos de perfil customizados
paymentsGestão de preços (Paywall), Checkouts, Pagamentos, Assinaturas recorrentes, integração de Gateways (Asaas, Fake) e Webhooks
spacesTodo o conteúdo: Posts, Comentários, Eventos, Cursos, Chat, DMs, Imagens, Reações, Reports, Moderação, Produtos Digitais (ProductItem)
mediaPipeline 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)
streamingAnalytics de playback (MediaPlaybackSession/Heartbeat) — separado de media (que só processa/produz o arquivo), ver ADR 0009
headlessAPI shim com shape JSON compatível com Circle.so
downloadsAnalytics de downloads (FileDownloadEvent): registro nos 3 fluxos (Drive, anexos de aula, produtos digitais) + sumários por alvo e dashboard admin (ADR 0016)
mcp_serverServidor MCP (FastMCP): autenticação via MCPApiKey, tools para gerenciamento por IA
webPá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.

CampoTipoDescrição
created_atDateTimeField(auto_now_add)Data de criação
updated_atDateTimeField(auto_now)Última atualização

4.2 CustomUser

Extends AbstractUser.

CampoTipoDescrição
avatarFileFieldFoto 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 username
  • avatar_url → URL do avatar ou fallback Gravatar
  • has_verified_email → verifica allauth EmailAddress

4.3 Community (Tenant Principal)

Cada comunidade é um tenant isolado.

CampoTipoDescrição
nameCharField(255)Nome da comunidade
slugSlugField(63, unique)Subdomínio (validado contra subdomínios reservados)
custom_domainCharField(255, unique, nullable)Domínio próprio opcional
custom_domain_verifiedBooleanField(default=False)Se o domínio próprio foi verificado
ownerForeignKey → CustomUser(PROTECT)Dono da comunidade
invite_onlyBooleanField(default=True)Se é apenas por convite
is_activeBooleanField(default=True)Se a comunidade está ativa
descriptionTextField(blank)Descrição da comunidade
default_languageCharField(10, default="pt-BR")Idioma primário padrão da comunidade
available_languagesJSONField(default=list)Lista de códigos de idiomas suportados (ex: ["pt-BR", "en-US"])
is_multilingualBooleanField(default=False)Se os recursos multilíngue e traduções estão ativos no tenant
timezoneCharField(50, default="America/Sao_Paulo")Fuso horário padrão da comunidade
currencyCharField(3, default="BRL")Moeda padrão para cobranças/paywalls
i18nJSONField(default=dict)Dicionário de traduções de campos dinâmicos por código de idioma
nav_configJSONField(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)

CampoTipoDescrição
userForeignKey → CustomUser(CASCADE)related_name: memberships
communityForeignKey → Community(CASCADE)related_name: memberships
roleCharFieldadmin, moderator, member
statusCharFieldactive, invited, banned
invite_tokenCharField(64, unique, nullable)Token de convite
invited_byForeignKey → CustomUser(SET_NULL, nullable)Quem convidou
joined_atDateTimeField(nullable)Data de adesão
muted_untilDateTimeField(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 único
  • accept_invite() → aceita convite, define status=active
  • is_muted() → verifica se está silenciado

4.5 PointTransaction (Gamificação)

CampoTipoDescrição
membershipForeignKey → Membership(CASCADE)related_name: point_transactions
actionCharFieldpost_created, comment_created, lesson_completed
pointsPositiveIntegerFieldPontos concedidos
content_typeForeignKey → ContentType(CASCADE, nullable)Para GenericFK
object_idPositiveBigIntegerField(nullable)Para GenericFK
targetGenericForeignKeyAlvo 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)

CampoTipoDescrição
communityForeignKey → Community(CASCADE)
nameCharField(100)Label exibido
keySlugField(100)Identificador único por comunidade
field_typeCharFieldtext, textarea, number, url, date, choice, multi_choice
optionsJSONField(default=list)Opções para choice/multi_choice
is_requiredBooleanField(default=False)Se é obrigatório
orderPositiveIntegerField(default=0)Ordem de exibição

4.7 ProfileFieldValue (Dados do Perfil Customizado)

CampoTipoDescrição
membershipForeignKey → Membership(CASCADE)related_name: profile_field_values
field_definitionForeignKey → ProfileFieldDefinition(CASCADE)
valueJSONField(nullable)Valor preenchido

Constraints: UNIQUE(membership, field_definition)


4.8 AccessGroup (Grupo — Controle de Acesso Granular)

CampoTipoDescrição
communityForeignKey → Community(CASCADE)
nameCharField(100)Nome do grupo
slugSlugField(100)Slug único por comunidade
descriptionTextField(blank)Descrição
membersM2M → Membership (through AccessGroupMembership)Membros do grupo

4.9 AccessGroupMembership (Through Model)

CampoTipoDescrição
access_groupForeignKey → AccessGroup(CASCADE)
membershipForeignKey → Membership(CASCADE)

Constraints: UNIQUE(access_group, membership)


4.10 APIKey

CampoTipoDescrição
communityForeignKey → Community(CASCADE)related_name: api_keys
scopesJSONField(default=list)Escopos de permissão
last_used_atDateTimeField(nullable)Último uso
created_byForeignKey → CustomUser(SET_NULL, nullable)Quem criou

4.11 SpaceGroup (Seção — Agrupamento na Sidebar)

CampoTipoDescrição
communityForeignKey → Community(CASCADE)
nameCharField(100)Nome do grupo
slugSlugField(100)Slug único por comunidade
orderPositiveIntegerField(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.

CampoTipoDescrição
communityForeignKey → Community(CASCADE)
space_groupForeignKey → SpaceGroup(CASCADE)Agrupamento na sidebar
nameCharField(100)Nome do espaço
slugSlugField(100)Slug único por comunidade
descriptionTextField(blank)Descrição
space_typeCharFieldbasic, chat, event, course, members, image
access_levelCharFieldpublic, private, secret
access_groupsM2M → AccessGroup(blank)Grupos com acesso
orderPositiveIntegerField(default=0)Ordem na sidebar

Manager customizado: SpaceQuerySet com visible_to(membership) — implementa regras de visibilidade:

  • public → listado para todos
  • private → listado mas acesso restrito
  • secret → 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

CampoTipoDescrição
spaceForeignKey → Space(CASCADE)Apenas em spaces basic
communityForeignKey → Community(CASCADE)
authorForeignKey → Membership(PROTECT)
bodyTextFieldConteúdo (Markdown/plain text)
is_pinnedBooleanField(default=False)Fixado no topo
is_lockedBooleanField(default=False)Trancado (sem novos comentários)
tagsM2M → Tag(blank)Tags do post
reactionsGenericRelation → Reaction

Ordering: ["-is_pinned", "-created_at"]

Validação: Posts só podem ser criados em spaces do tipo basic.


4.14 Comment

CampoTipoDescrição
postForeignKey → Post(CASCADE)
communityForeignKey → Community(CASCADE)
authorForeignKey → Membership(PROTECT)
parentForeignKey → self(CASCADE, nullable)Aninhamento máximo de 1 nível
bodyTextFieldConteúdo
reactionsGenericRelation → Reaction

Constraints: Respostas com máximo 1 nível de profundidade. Resposta deve pertencer ao mesmo post que o pai.


4.15 Event

CampoTipoDescrição
spaceForeignKey → Space(CASCADE)Apenas em spaces event
communityForeignKey → Community(CASCADE)
hostForeignKey → Membership(PROTECT)
titleCharField(200)Título do evento
descriptionTextField(blank)Descrição
starts_atDateTimeFieldData/hora de início
ends_atDateTimeFieldData/hora de término (validado: deve ser após starts_at)
timezoneCharField(64)Timezone (validado contra available_timezones())
virtual_meeting_urlURLField(blank)URL da reunião virtual
locationCharField(255, blank)Local físico

4.16 RSVP

CampoTipoDescrição
eventForeignKey → Event(CASCADE)
membershipForeignKey → Membership(CASCADE)
statusCharFieldgoing, interested, not_going

Constraints: UNIQUE(event, membership)


4.17 Tag

CampoTipoDescrição
communityForeignKey → Community(CASCADE)
nameCharField(50)Nome da tag
slugSlugField(50)Slug único por comunidade

4.18 Reaction (Polimórfico)

CampoTipoDescrição
membershipForeignKey → Membership(CASCADE)
content_typeForeignKey → ContentType(CASCADE)
object_idPositiveBigIntegerField
targetGenericForeignKeyPost, Comment ou Image
emojiCharFieldlike, love, celebrate, support, insightful, curious

Constraints: UNIQUE(membership, content_type, object_id). Só é possível reagir a Post, Comment ou Image.


4.19 Image

CampoTipoDescrição
spaceForeignKey → Space(CASCADE)Apenas em spaces image
communityForeignKey → Community(CASCADE)
authorForeignKey → Membership(PROTECT)
fileFileFieldImagem otimizada (WebP, máx. 1200px, EXIF-transpose) — re-encodada no serializer via apps.media.services.optimize, salva em gallery-images/{uuid}.webp
captionCharField(280, blank)Legenda
reactionsGenericRelation → Reaction

4.20 Module (Seção do Curso)

CampoTipoDescrição
spaceForeignKey → Space(CASCADE)Apenas em spaces course
communityForeignKey → Community(CASCADE)
titleCharField(200)Título do módulo
descriptionTextField(blank)Descrição
orderPositiveIntegerField(default=0)Ordem

4.21 Lesson

CampoTipoDescrição
moduleForeignKey → Module(CASCADE)
spaceForeignKey → Space(CASCADE)Denormalizado do módulo
communityForeignKey → Community(CASCADE)
titleCharField(200)Título da aula
bodyTextField(blank)Conteúdo da aula
orderPositiveIntegerField(default=0)Ordem

4.22 LessonProgress

CampoTipoDescrição
lessonForeignKey → Lesson(CASCADE)
membershipForeignKey → Membership(CASCADE)
completed_atDateTimeField(nullable)Data de conclusão

Constraints: UNIQUE(lesson, membership)


4.23 ChatMessage

CampoTipoDescrição
spaceForeignKey → Space(CASCADE)Apenas em spaces chat
communityForeignKey → Community(CASCADE)
authorForeignKey → Membership(PROTECT)
bodyTextFieldMensagem
edited_atDateTimeField(nullable)Data de edição
deleted_atDateTimeField(nullable)Soft-delete (tombstone para WebSocket)

4.24 DMThread

CampoTipoDescrição
communityForeignKey → Community(CASCADE)
membership_aForeignKey → Membership(CASCADE)Membro com menor ID
membership_bForeignKey → 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

CampoTipoDescrição
threadForeignKey → DMThread(CASCADE)
senderForeignKey → Membership(PROTECT)
bodyTextFieldMensagem
edited_atDateTimeField(nullable)Data de edição
deleted_atDateTimeField(nullable)Soft-delete

4.26 Report (Polimórfico)

CampoTipoDescrição
reporterForeignKey → Membership(PROTECT)
communityForeignKey → Community(CASCADE)
content_typeForeignKey → ContentType(CASCADE)
object_idPositiveBigIntegerField
targetGenericForeignKeyPost, Comment, Image ou ChatMessage
reasonCharFieldspam, harassment, off_topic, other
detailsTextField(blank)Detalhes adicionais
statusCharFieldopen, resolved, dismissed
resolved_byForeignKey → Membership(SET_NULL, nullable)
resolved_atDateTimeField(nullable)

Constraints: UNIQUE(reporter, content_type, object_id)


4.27 SpaceModerator

CampoTipoDescrição
spaceForeignKey → Space(CASCADE)
membershipForeignKey → 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

CampoTipoDescrição
userForeignKey → CustomUser(CASCADE)related_name: bookmarks
communityForeignKey → Community(CASCADE)related_name: bookmarks
postForeignKey → Post(CASCADE)related_name: bookmarks

Constraints: UNIQUE(user, post)


4.29 PostFollower

CampoTipoDescrição
userForeignKey → CustomUser(CASCADE)related_name: post_follows
postForeignKey → Post(CASCADE)related_name: followers

Constraints: UNIQUE(user, post)


4.30 ChatRoom

CampoTipoDescrição
communityForeignKey → Community(CASCADE)related_name: chat_rooms
uuidUUIDField(unique)Identificador para URLs
kindCharFielddirect, group_chat
nameCharField(255, blank)Nome da sala (grupos)
created_byForeignKey → Membership(SET_NULL, nullable)Quem criou

Ordering: -created_at


4.31 ChatRoomMember

CampoTipoDescrição
chat_roomForeignKey → ChatRoom(CASCADE)related_name: members
membershipForeignKey → Membership(CASCADE)related_name: chat_room_memberships
last_read_atDateTimeField(nullable)Última leitura

Constraints: UNIQUE(chat_room, membership)


4.32 ChatRoomMessage

CampoTipoDescrição
chat_roomForeignKey → ChatRoom(CASCADE)related_name: messages
senderForeignKey → Membership(SET_NULL, nullable)related_name: chat_room_messages
bodyTextField(blank)Texto da mensagem
rich_text_bodyJSONField(nullable)Formato rico (Tiptap)
parent_messageForeignKey → self(SET_NULL, nullable)Para threads
edited_atDateTimeField(nullable)Data de edição
deleted_atDateTimeField(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.

CampoTipoDescrição
membershipForeignKey → Membership(CASCADE)related_name: mcp_api_keys
nameCharField(100, default="MCP key")Nome de identificação da chave
prefixCharField(16, db_index)Prefixo em texto plano para identificação (primeiros 16 chars)
key_hashCharField(64, unique)Hash SHA-256 da chave bruta
last_used_atDateTimeField(nullable)Data do último uso
revoked_atDateTimeField(nullable)Data de revogação

Propriedades e Métodos:

  • is_activeTrue se revoked_at for None
  • create_key(membership, name) → gera nova chave mcp_<token> e armazena o hash
  • hash_key(raw_key) → calcula o hash SHA-256 da chave bruta
  • mark_used() → atualiza last_used_at
  • revoke() → define revoked_at para 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.

CampoTipoDescrição
mediaForeignKey → Media(CASCADE)Mídia reproduzida
userForeignKey → CustomUser(SET_NULL, nullable)Usuário autenticado (se houver)
session_idUUIDField(unique, db_index)UUID da sessão gerado pelo player
device_typeCharField(50)desktop, mobile, etc
browserCharField(50)User-agent/Navegador
total_watch_timeFloatField(default=0.0)Tempo total assistido em segundos
max_positionFloatField(default=0.0)Maior posição alcançada no vídeo (s)
completion_rateFloatField(default=0.0)Taxa de conclusão (0.0 a 1.0)
is_completedBooleanField(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.

CampoTipoDescrição
sessionForeignKey → MediaPlaybackSession(CASCADE)Sessão correspondente
mediaForeignKey → Media(CASCADE)Mídia
current_timeFloatFieldPosição atual do player em segundos
watch_time_deltaFloatFieldTempo assistido desde o último ping
playback_rateFloatField(default=1.0)Velocidade de reprodução
qualityCharField(20)Qualidade HLS (1080p, 720p, auto)
rebuffer_eventsIntegerField(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.).

CampoTipoDescrição
nameCharField(255, blank)Nome descritivo da oferta
descriptionTextField(blank)Descrição do Paywall
spaceOneToOneField → Space(CASCADE, nullable)Espaço monetizado
content_typeForeignKey → ContentType(CASCADE, nullable)Alvo dinâmico (GenericFK)
object_idPositiveIntegerField(nullable)ID do alvo dinâmico
is_activeBooleanField(default=True)Status do Paywall

4.38 PaywallPrice (apps.payments)

Preço e variante de cobrança (moeda, ciclo) associada a um Paywall.

CampoTipoDescrição
paywallForeignKey → Paywall(CASCADE)Paywall pertencente
price_centsPositiveIntegerFieldPreço em centavos
currencyCharField(3, default="BRL")Moeda (BRL, USD, EUR)
payment_typeCharField(20, default="one_time")one_time ou subscription
billing_cycleCharField(20, nullable)monthly, quarterly, semestral, yearly
is_defaultBooleanField(default=False)Indica se é a variante padrão por moeda
is_activeBooleanField(default=True)Status do preço

4.39 Payment (apps.payments)

Registro de transação e status do checkout.

CampoTipoDescrição
paywallForeignKey → Paywall(CASCADE)Paywall cobrado
priceForeignKey → PaywallPrice(CASCADE, nullable)Variante de preço selecionada
membershipForeignKey → Membership(CASCADE, nullable)Comprador autenticado (se houver)
guest_emailEmailField(nullable)E-mail de comprador convidado/não-autenticado
guest_nameCharField(255, nullable)Nome do comprador convidado
statusCharField(20, default="pending")pending, completed, failed, refunded
gatewayCharField(20, default="fake")fake, asaas, etc.
payment_methodCharField(20, nullable)pix, card, boleto
gateway_payment_idCharField(255, nullable)ID no gateway
gateway_customer_idCharField(255, nullable)ID do cliente no gateway
subscription_idCharField(255, nullable)ID da assinatura no gateway
completed_atDateTimeField(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.

CampoTipoDescrição
spaceForeignKey → Space(CASCADE)Apenas em spaces digital_product
communityForeignKey → Community(CASCADE)
titleCharField(200)Título do item
descriptionTextField(blank)Descrição
fileFileFieldArquivo principal (tablatura/preset/midi/sample pack)
file_typeCharField(20, default="other")tablature, preset, midi, sample_pack, other
tuningCharField(50, blank)Afinação (tabs) — ex. Standard, Drop D
difficultyCharField(20, blank)beginner, intermediate, advanced
instrumentCharField(20, blank)guitar, bass, keys, drums, other
keyCharField(20, blank)Tom musical (tabs)
dawCharField(50, blank)DAW alvo (presets) — ex. Ableton, Logic
pluginCharField(100, blank)Plugin alvo (presets)
genreCharField(50, blank)Gênero (presets/samples)
is_free_previewBooleanField(default=False)Libera item + vídeos sem compra do pacote
orderPositiveIntegerField(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).

CampoTipoDescrição
communityForeignKey → Community(CASCADE)related_name: download_events
content_type / object_id / targetGFKAlvo do download: DriveFile, Media ou ProductItem
membershipForeignKey → Membership(SET_NULL, nullable)Baixador; nulo para visitantes anônimos (aulas grátis)
ip_addressGenericIPAddressField(nullable)IP do cliente (proxy-safe, via apps.utils.middleware.get_client_ip)
user_agentTextField(blank)User-Agent (truncado a 1000 chars)
referrerCharField(2048, blank)Referer HTTP (truncado; CharField de propósito, referrers malformados não podem falhar o tracking)
mime_typeCharField(255, blank)Snapshot MIME no momento do download
file_sizePositiveBigIntegerField(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:

CampoTipoDescrição
media_typeCharField (choices, nullable)video|audio|image; nulo até import_media_from_url_task detectar (upload direto já vem setado)
statusCharField (choices)created→importing→uploaded→transcoding→extracting_audio→transcribing→diarizing(cond.)→summarizing→ready|failed
failed_stageCharField (choices, blank)Snapshot do status no momento da falha (distinto do status ao vivo, usado por MediaReprocessView)
error_messageTextField(blank)Erro da última falha
roleCharField (choices, blank)Slot nomeado sales_video|demo_video — vídeo de curso a nível de Space (Space.is_accessible_to gate)
visibilityCharField (choices)public|unlisted|private (Standalone Media)
fileFileFieldOriginal enviado
thumbnail / thumbnail_smallFileField(nullable)Variantes WebP 1200px/256px (imagem) ou poster de vídeo
hls_master_playlistFileField(nullable, max_length=500)Playlist HLS master; hls_video_url (property) trata URL externa (import via /media/import-hls/) sem passar pelo storage
renditionsJSONField(list)[{quality, width, height, bitrate_kbps, playlist_path}, ...] — ladder HLS efetivamente gerado
extracted_audioFileField(nullable)Trilha de áudio extraída (extract_audio_task), reusada pela transcrição
animated_preview / sprite_vttFileField(nullable)Preview WebP animado (3s) / spritesheet+VTT pra scrubbing
duration_seconds / width / heightFloat / Int / Int (nullable)Probados via ffprobe
waveform_peaksJSONField(list)Picos de amplitude normalizados (0-1), pra UI de waveform (voice comments)

Transcrição & IA (resumo/chapters/highlights):

CampoTipoDescrição
transcript_textTextField(blank)Texto plano (com label de speaker se diarizado)
transcript_rawJSONField(nullable)Resposta bruta do provider (Deepgram) — auditoria/reprocessamento
transcript_jsonJSONField(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_vttFileField(nullable)Legendas geradas (apps.media.services.captions)
summaryTextField(blank)Resumo gerado por LLM (apps.media.services.summarization)
chaptersJSONField(list)[{start, end, title}, ...] — marcadores de navegação, não confundir com VideoHighlight (4.43)
highlightsJSONField(list)[{start, end, label}, ...] — marcadores de destaque, mesma origem/ressalva de chapters
highlights_status / highlights_error_messageCharField (choices) / TextFieldStatus 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/):

CampoTipoDescrição
stems_status / stems_error_messageCharField (choices) / TextFieldPar isolado — uma falha aqui nunca reverte um status já READY pra FAILED
stems_modelCharField(blank)Modelo Demucs usado (htdemucs, 4 ou 6 stems — apps.media.demucs_catalog)
stemsJSONField(dict){"vocals": "media-stems/<id>/vocals.wav", ...}

Dubbing (tradução + TTS, opcional, sob demanda — POST /media/<id>/dub/):

CampoTipoDescrição
dubbing_status / dubbing_error_messageCharField (choices) / TextFieldPar isolado, mesma razão de stems_status. Global, não por-idioma — só um dub roda por vez por media
dubsJSONField(dict){"en": "media-dubs/<id>/en.mp4", ...} — um entry por idioma já dublado, acumulativo
dubbing_segments_total / dubbing_segments_donePositiveIntegerFieldProgresso 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).

CampoTipoDescrição
mediaForeignKey → Media(CASCADE)related_name: video_highlights
title / hook / descriptionCharField / TextField / TextFieldGerados pela passada "curator"; hook é a legenda dos primeiros ~2s
start_ms / end_msPositiveIntegerFieldBounds gerais (min/max) através de todos os segments, pós-snap
segmentsJSONField (list)[{"start_ms", "end_ms"}, ...] na ordem de corte (não necessariamente cronológica) — 1 entrada = corte simples, 2+ = clipe composto/costurado
scoreFloatFieldPotencial viral, 1-10, da passada "curator"
reasoningTextFieldJustificativa de 1 frase para o score
statusCharField (choices)candidate|approved|rejected — estado de curadoria da IA
render_statusCharField (choices)not_requested|processing|ready|failed — independente de status; nunca alterado por um status de curadoria e vice-versa
render_styleCharField (choices)raw (implementado) ou vertical_story (fast-follow, não implementado ainda)
rendered_fileFileField(nullable)Clipe cortado/concatenado, produzido por render_video_highlight_task
render_error_messageTextField(blank)Erro da última tentativa de render
cover_frameFileField(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.

CampoTipoDescrição
mediaForeignKey → Media(CASCADE)related_name: pipeline_jobs
job_typeCharField (choices)highlights_generate|highlight_render|highlight_cover_frame
statusCharField (choices)running|done|failed
progressPositiveIntegerField(default=0)0-100, best-effort (a maioria dos job_types de hoje não reporta progresso incremental)
payloadJSONField (dict)Args de entrada; carrega chaves de escopo como highlight_id quando o job_type não é Media-level
resultJSONField(nullable)Saída no sucesso
errorTextField(blank)Erro no on_failure da task Celery correspondente
completed_atDateTimeField(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 -- Post

Generic Foreign Keys (Alvos Polimórficos)

ModeloCampoAlvos Possíveis
ReactiontargetPost, Comment, Image
ReporttargetPost, Comment, Image, ChatMessage
PointTransactiontargetPost, Comment, Lesson
FileDownloadEventtargetDriveFile, Media (anexo de aula), ProductItem

6. Sistema de Permissões

Matriz de Permissões por Role e Recurso

CategoriaAção / RecursoAdmin (Role.ADMIN)Moderador (Role.MODERATOR)Space Moderator (SpaceModerator)Membro Comum (Role.MEMBER)DRF Permission Class / Rule
Spaces & CursosCriar/Editar/Deletar Spaces e SpaceGroupsIsAdminOrModeratorForWrite
Criar/Editar/Deletar Módulos, Lessons e TagsIsAdminOrModeratorForWrite
Acesso a Spaces Privados/Secretos sem membershipSpace.is_accessible_to()
Gerenciar SpaceModeratorIsAdminOrModeratorForWrite
Posts & ConteúdoCriar/Editar/Deletar conteúdo próprioIsAuthorOrModerator, 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) alheiasIsDMParticipant, IsDMSender
Membros & ModeraçãoBanir/Desbanir/Mutar membros MEMBERCanModerateMembership
Banir/Mutar/Alterar role de MODERATORCanModerateMembership
Banir/Mutar/Alterar role de ADMINCanModerateMembership
Ações de moderação em si próprio (self-mute/ban)CanModerateMembership

apps.communities.permissions

PermissãoEscopoDescrição
RequiresCommunityViewHost header deve resolver para uma comunidade conhecida
RequiresActiveMembershipViewRequest deve ter uma membership ativa
IsAuthenticatedOrHasAPIKeyViewAuth por usuário ou API key
CanModerateMembershipObjectVerifica roles admin/moderator para ações de ban/mute

apps.spaces.permissions

PermissãoEscopoDescrição
HasSpaceAccessObjectDelega para Space.is_accessible_to()
IsAuthorOrModeratorObjectAutor, admin/moderator, ou SpaceModerator
IsAdminOrModeratorForWriteViewAdmin/moderator para escritas estruturais
IsAdminOrModeratorViewAdmin/moderator para todos os métodos (fila de moderação)
IsNotLockedViewPost não pode estar trancado (para comentários)
IsNotMutedViewMembership não pode estar silenciada (para escritas)
IsDMParticipantObjectDeve ser participante do DM thread
IsDMSenderObjectDeve ser o remetente da DM message (sem bypass de mod)

7. Middlewares

TenantResolutionMiddleware

Arquivo: apps/communities/middleware.py

  1. Extrai o host do request
  2. Busca Community por slug ou custom_domain
  3. Anexa request.community
  4. Se autenticado, busca Membership e anexa request.membership (lazy)
  5. Modo debug: suporta header X-Community-Slug para override

TenantWebsocketAuthMiddleware

Arquivo: apps/communities/channels_middleware.py

  1. Resolve comunidade a partir do Host
  2. Autentica via token de uso único armazenado no Redis (TTL 30s)
  3. 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 + membership
  • GET/PATCH /api/v1/community/profile-fields/ — Campos de perfil customizados
  • POST /api/v1/community/ws-token/ — Token para WebSocket
  • GET /api/v1/community/leaderboard/ — Ranking por pontos
  • POST /api/v1/community/members/<id>/ban/ — Banir membro
  • POST /api/v1/community/members/<id>/unban/ — Desbanir membro
  • POST /api/v1/community/members/<id>/mute/ — Silenciar membro
  • POST /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 (requer user.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 como Membership com role ADMIN e status ACTIVE via post_save signal.
  • POST /api/v1/communities/invites/ — Convite em lote de membros para uma comunidade. Aceita lista de e-mails e role (member|moderator). Para cada e-mail: cria Membership com status INVITED (ou reutiliza existente via get_or_create), e dispara e-mail HTML de convite via send_community_invitation_email (template emails/community_invitation.html). Requer que o requester seja ADMIN da comunidade.
  • GET /api/v1/users/me/ — Estendido com can_create_community, has_owned_community e owned_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 o uid (UUID). Admin/moderador (IsAdminOrModerator, mesmo gate do MediaAnalyticsSummaryView).
  • GET /api/v1/community/admin/analytics/ — dashboard admin; inclui a seção files (total_downloads, top_files com label/tipo/id/downloads resolvidos por modelo via GFK, timeline 30d).
  • 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) e GET /api/v1/product-items/{item_id}/file/ (produto digital) criam FileDownloadEvent antes 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 dispara enqueue_media_processing.
  • POST /api/v1/media/import-url/ — cria Media em IMPORTING a partir de uma URL (YouTube/Vimeo via yt-dlp, ou download direto), dispara import_media_from_url_task.
  • POST /api/v1/media/import-hls/ — registra um vídeo HLS já hospedado externamente, sem download/transcode; Media nasce READY.
  • 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, se failed_stage=IMPORTING); só se status=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, fila stems (worker-stems). Rejeita se já PROCESSING ou se media_type=image.
  • POST /api/v1/media/<id>/dub/ — Dublagem sob demanda ({target_language, translation_provider?, tts_engine?}), fila dubbing (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 do tusd-media, autenticado por secret compartilhado, não por sessão).

Media — Highlights com IA (ADR 0018):

  • POST /api/v1/media/<id>/highlights/generate/ — dispara generate_video_highlights_task ({force: bool}). 400 se sem transcrição ou já em progresso. Apenas o uploader.
  • GET /api/v1/media/<id>/highlights/ — lista VideoHighlight do media, ordenado por -score.
  • GET/PATCH /api/v1/media/highlights/<id>/ — detalhe (poll de status) / atualização de status (candidate\|approved\|rejected).
  • POST /api/v1/media/highlights/<id>/render/ — dispara render_video_highlight_task ({style}, só raw aceito na v1). Apenas o uploader.
  • POST /api/v1/media/highlights/<id>/cover-frame/ — dispara select_highlight_cover_frame_task ({strategy: exact|thumbnail|ai}, default ai). Apenas o uploader.
  • GET /api/v1/media/<id>/pipeline-jobs/ — lista PipelineJob do 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 de MediaSeparateStemsView/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 atual
  • profile — Dados do perfil com campos customizados
  • community_members — Listagem de membros buscável
  • community_members/<id>/public_profile — Perfil público (posts, comments, followers)
  • community_members/<id>/spaces — Spaces do membro
  • search/community_members — Busca por membros

Spaces (8):

  • spaces / spaces/home — Listagem de spaces e home feed
  • spaces/<id>/posts — Posts de um space
  • spaces/<id>/posts/<id> — Detalhe de um post
  • spaces/<id>/join / leave — Entrar/sair de space
  • spaces/<id>/topics — Tags do space
  • spaces/<id>/bookmarks — Bookmarks do space

Posts/Comments (6):

  • posts — Criar post
  • posts/<id>/comments — Listar/criar comentários
  • posts/<id>/comments/<id> — Editar/deletar comentário
  • posts/<id>/user_likes — Curtir/descurtir post
  • posts/<id>/post_followers — Seguir/deixar de seguir post
  • comments/<id>/user_likes / replies — Curtir comentário, criar reply

Notifications (7):

  • notifications — Listar notificações
  • notifications/new_notifications_count — Contagem não lidas
  • notifications/mark_all_as_read — Marcar todas como lidas
  • notifications/<id>/mark_as_read / archive — Ações individuais
  • space_notification_details — Detalhes por space
  • notification_preferences/<medium> — Preferências por canal
  • notification_preferences/<medium>/spaces — Preferências por space

Bookmarks (2):

  • bookmarks — Listar/criar bookmarks
  • bookmarks/<postId> — Deletar bookmark

Events (4):

  • community_events — Eventos da comunidade
  • events/<id>/event_attendees — Listar/confirmar/cancelar presença
  • spaces/<id>/events/<id>/recurring_events — Eventos recorrentes
  • spaces/<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 chat
  • messages/<uuid>/chat_room_messages — Listar/enviar mensagens
  • messages/<uuid>/mark_all_as_read — Marcar como lido
  • messages/unread_chat_rooms — Salas não lidas
  • chat_threads / chat_threads/<id> — Threads DM

Courses (3):

  • courses/<id>/sections — Módulos do curso
  • courses/<id>/lessons/<id> — Detalhe da aula; inclui video_media_id (uid do Media do vídeo principal) para o player apontar a telemetria de playback (ADR 0016; null no stub locked)
  • courses/<id>/lessons/<id>/progress — Progresso da aula

Outros (5):

  • advanced_search — Busca full-text em posts
  • invitation_links/<token>/join — Aceitar convite
  • page_profile_fields — Campos de perfil da página
  • community_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_name
  • reactions — Criar reação

Micro-apps (2):

  • community/micro-apps — GET: apps habilitados com config merged (defaults + overrides, chaves desconhecidas descartadas). Staff com ?include_disabled=true recebe o catálogo inteiro com is_enabled.
  • community/micro-apps/<app_id> — PATCH (admins/moderadores): toggle is_enabled, order e merge de config validado antes do save (400 {"error", "details"}; PATCH inválido não cria row). Catalogo em apps/communities/micro_apps.py; rows em CommunityMicroApp. 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) e invitation_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:

GateUso
_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:

  1. 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.

  2. Autorização em cima do gate reusa as permission classes do /v1: IsAuthorOrModerator, IsAdminOrModerator, IsNotMuted, IsNotLocked — não reimplementar checagens frouxas.

  3. Locked/secret → 404, nunca 200 com dados. CourseLessonDetailView.get é a única exceção permitida (serve o stub locked=True com sections: []), e está na allowlist explícita do teste.

  4. 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)
  5. CI faz cumprir a regra: StructuralAccessGuardTests (em apps/headless/tests/test_headless_access_regressions.py) varre o código-fonte de headless/views.py e falha se um get_object_or_404 de modelo de conteúdo (Post/Comment/Event/Lesson/Quiz/Announcement) aparecer num handler sem gate. Testes comportamentais em apps/headless/tests/test_visibility_contracts.py cobrem o mesmo contrato com outsider/insider.

8.3 Autenticação JWT

Endpoint: POST /api/v1/auth/token/

json
{
  "email": "user@example.com",
  "password": "senha123"
}

Resposta:

json
{
  "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 arquivo project/asgi.py através do Starlette.
  • O Starlette roteia requisições em /mcp para a aplicação HTTP stateless do FastMCP, enquanto repassa as demais requisições ao ProtocolTypeRouter (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 Membership associada deve ter status="active".
  • O contexto da requisição fornece automaticamente:
    • user: Usuário associado à membership
    • community: Tenant da comunidade
    • membership: 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óduloDescrição das Ferramentas
tools.communitiesConsulta detalhes da comunidade e contagem de membros
tools.spacesListagem e gestão de espaços e grupos de espaços
tools.postsListagem, criação, busca e moderação de posts e comentários
tools.usersConsulta de perfis de membros e informações de usuários

9. WebSockets

URLs

PadrãoConsumerUso
ws/chat/spaces/<space_id>/ChatConsumerChat ao vivo em spaces
ws/dm/threads/<thread_id>/DMConsumerMensagens diretas

Autenticação WebSocket

  1. Usuário faz POST /community/ws-token/ via REST
  2. Token de uso único é armazenado no Redis com TTL de 30 segundos
  3. WebSocket handshake envia o token
  4. TenantWebsocketAuthMiddleware consome 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çãoPontos
post_created10
comment_created5
lesson_completed15

Níveis

NívelPontos Necessários
10
2100
3300
4700
51500

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

SinalAção
post_save(Community)Auto-cria Membership admin para o owner da comunidade

apps.users/signals.py

SinalAção
user_signed_upNotifica admins de novos registros
email_confirmedDefine 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ávelPadrãoDescrição
Core & Segurança
SECRET_KEYdjango-insecure-...Chave secreta de criptografia do Django
DEBUGTrueAtiva o modo de depuração e profiler
ALLOWED_HOSTS*Domínios permitidos para requisições
ROOT_DOMAINlocalhostDomínio apex para resolução de subdomínios multi-tenant
FRONTEND_URLhttp://localhost:3000URL da aplicação frontend Next.js/Web
USE_HTTPS_IN_ABSOLUTE_URLSFalseForça esquemas HTTPS em links absolutos gerados
CSRF_TRUSTED_ORIGINSlocalhost: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_URLPostgreSQL localURL de conexão PostgreSQL (Postgres local ou Neon)
CATALOG_DATABASE_URLNoneURL do Postgres do serviço externo catalog
DJANGO_DATABASE_CONN_MAX_AGE60Tempo máximo de reutilização de conexões DB (segundos)
REDIS_URLredis://127.0.0.1:6379URL do Redis (usado por Cache, Celery e WebSockets)
Armazenamento (S3/MinIO/R2)
AWS_STORAGE_BUCKET_NAMEproject-mediaNome do bucket S3/MinIO para mídias
AWS_S3_ENDPOINT_URLhttp://127.0.0.1:9000Endpoint do MinIO/Cloudflare R2/S3
AWS_S3_REGION_NAMEus-east-1Região do serviço S3
AWS_ACCESS_KEY_IDminioadminAccess Key do S3/MinIO
AWS_SECRET_ACCESS_KEYminioadminSecret Key do S3/MinIO
DRIVE_TUS_WEBHOOK_SECRETchange-me-drive-tusChave secreta compartilhada com o servidor tusd
DRIVE_TUS_PUBLIC_URLhttp://localhost:1080URL pública do servidor de upload resumível tusd
Transcrição & Mídia (Deepgram & FFmpeg)
FFMPEG_BINARYffmpegCaminho do binário do FFmpeg no sistema
FFPROBE_BINARYffprobeCaminho do binário do FFprobe no sistema
FFMPEG_TIMEOUT_SECONDS1800Timeout máximo para execuções do FFmpeg
DEEPGRAM_API_KEY""Chave de API da Deepgram para transcrição de áudio
DEEPGRAM_MODELnova-3Modelo de transcrição do Deepgram (nova-3, whisper-large, etc.)
DEEPGRAM_DIARIZE_MODELlatestModelo de diarização de oradores do Deepgram
DEEPGRAM_DETECT_LANGUAGEFalseAtiva detecção automática de idioma no Deepgram
Resumos por IA & Tradução LLM
AI_SUMMARY_BASE_URLhttps://api.openai.com/v1URL 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_MODELgpt-4o-miniModelo da LLM usado para geração de resumos
AI_VISION_MODELgpt-4o-miniModelo 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_URLhttp://localhost:5000URL do servidor de tradução LibreTranslate
TRANSLATION_PRIMARY_PROVIDERlibretranslateProvedor primário de tradução (libretranslate, argos, llm)
IA Avançada (Stems, Dublagem, Diarização Local)
DEMUCS_BINARYdemucsBinário do Demucs para separação de faixas de áudio
DEMUCS_MODELhtdemucsModelo de IA para separação de áudio (htdemucs)
DEMUCS_DEVICEcpuDispositivo de processamento do Demucs (cpu ou cuda)
DUBBING_TRANSLATION_PROVIDERargosProvedor de tradução do pipeline de dublagem (argos ou llm)
DUBBING_TTS_ENGINEpiperMotor de síntese de voz para dublagem (piper)
PIPER_BINARYpiperBinário do Piper TTS no sistema
PIPER_VOICES_DIRpiper-voicesDiretório de modelos de vozes do Piper
DIARIZATION_ENGINEresemblyzerEngine de diarização local (resemblyzer ou pyannote)
HUGGINGFACE_TOKEN""Token do HuggingFace (necessário para pyannote)
YTDLP_BINARYyt-dlpBinário do yt-dlp para importação de vídeos por URL
Live Rooms & Streaming
VIDEO_PROVIDERlivekitProvedor de vídeo ao vivo para novas salas (livekit ou hms)
LIVEKIT_URLhttp://localhost:7880URL do servidor LiveKit self-hosted
LIVEKIT_API_KEYdevkeyAPI Key do LiveKit
LIVEKIT_API_SECRETsecretAPI Secret do LiveKit
Pagamentos (Asaas)
ASAAS_ACCESS_TOKEN""Token da API do gateway de pagamento Asaas
ASAAS_BASE_URLhttps://sandbox.asaas.com/api/v3Endpoint da API Asaas (Sandbox/Produção)
ASAAS_WEBHOOK_SECRETchange-me-asaas-webhookChave 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_RATE0.1Taxa de amostragem de rastreamento de performance
GLITCHTIP_PORT8088Porta do container local do Glitchtip (make glitchtip-start)

Configurações Chave

ConfiguraçãoValor
JWT Access Token30 minutos
JWT Refresh Token14 dias
JWT RotaçãoHabilitada
JWT BlacklistHabilitado
Throttling (user)1000/hora
Throttling (API key)5000/hora
Cache (DEBUG)DummyCache
Cache (produção)RedisCache
StorageS3 (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:

WorkerFilas (-Q)Tarefas ExecutadasPerfil / Requisitos
worker-maindefault, celery, critical, media-importtranscribe_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-heavyheavytranscode_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-stemsstemsseparate_stems_task (Demucs)IA/ML heavy (PyTorch)
worker-dubbingdubbingdub_media_task, dub_segment_task, prepare_dub_background_task, finalize_dub_taskIA/ML heavy (Piper TTS + Argos Translate)
worker-diarizationdiarizationdiarize_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

CommandAppDescrição
seedapps.webPopula o banco com dados de exemplo
bootstrap_celery_tasksapps.webCria tarefas periódicas no django-celery-beat
send_test_emailapps.webTesta configuração de email
promote_user_to_superuserapps.usersPromove usuário a superuser
sync_catalogapps.spacesSincroniza 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étodoEndpointDescriçã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étodoEndpointDescriçã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étodoEndpointDescriçã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étodoEndpointDescriçã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étodoEndpointDescriçã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étodoEndpointDescrição
GET/POST/DELETE/mcp/Endpoint do Servidor MCP (FastMCP / SSE / Stateless HTTP)

Documentação da API

EndpointDescriçã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, ...)ContentFile nomeado .webp para atribuição direta em FileField (usado por serializers/views).
  • optimize_image_to_path(input_path, output_path, ...) → variante para o pipeline Celery; lança ImageOptimizationError se 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 MediaTamanhoUso
thumbnail1200pxBanners, cover photo, feed
thumbnail_small256pxAvatares, ícones, logos

Onde cada imagem é otimizada:

SuperfícieCaminhoDetalhe
Galeria Imageapps/spaces/serializers/images.py (ImageSerializer.create/update)Síncrono, WebP 1200px
Avatar (templates Django)apps/users/views.py::upload_profile_imageSíncrono, WebP 256px
Avatar/cover/banner/icon/logo (SPA/headless)apps/headless/views.py::_pick_media_fileUsa 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).

Strum — Documentação.