Aulameet Voltar ao inicio

Sobre o Sistema Livre

AulaMeet foi construido do zero por um unico programador brasileiro. Aqui voce ve quem fez, como cada modulo funciona por dentro, as decisoes tecnicas e os codigos reais que fazem tudo isso rodar.

PHP Puro JavaScript Vanilla WebRTC MySQL Multi-tenant Groq / LLaMA 3.3 Cloudflare R2 Canvas API Sem frameworks pesados
O Desenvolvedor

Quem fez o AulaMeet

JF
Jonatas de Aguiar Mota Fontelis
Bacharel em Direito Mestrando em Direito Advogado Programador de Sistemas Estudante de Fisica e Robotica
Jonatas Fontelis e um profissional multidisciplinar do Maranhao, Brasil. Bacharel em Direito e atualmente Mestrando em Direito, atua como advogado e paralelamente desenvolveu o AulaMeet completamente sozinho — sem equipe, sem investimento externo, sem framework pesado.

Autodidata em programacao, domina PHP, JavaScript, MySQL, WebRTC, integracoes com IA (Groq/LLaMA), armazenamento em nuvem (Cloudflare R2, Google Drive) e construcao de sistemas web completos do zero. Tambem e estudante de Sistemas de Informacao e apaixonado por Fisica e Robotica aplicada.

O AulaMeet nasceu da necessidade real de professores de concursos que precisavam de uma plataforma completa, brasileira e acessivel. Em vez de pagar por ferramentas caras e fragmentadas, Jonatas construiu tudo: videoconferencia propria com WebRTC, matricula digital com contrato juridico, IA para simulados, trilha de etapas gamificada e muito mais.

"Construi o que eu precisava. Se funciona pra mim, funciona pra voce tambem. E de graca."
💛
Apoie o projeto via PIX
05294561336
Titular: Jonatas de Aguiar Mota Fontelis
Codigo Aberto

Como cada modulo funciona

Cada modulo foi construido do zero, sem frameworks pesados. PHP puro, JavaScript vanilla, MySQL e muita logica bem pensada.

🎥 Aula ao Vivo — O Modulo Principal
WebRTC peer-to-peer, lousa digital, gravacao automatica, chat, reacoes — tudo sem terceiros
DESTAQUE

Por que isso e impressionante?

A maioria das plataformas usa Zoom, Jitsi ou Daily.co — servicos pagos e externos. O AulaMeet implementou WebRTC do zero: sinalizacao via banco MySQL, conexao peer-to-peer direta, lousa digital com Canvas API, suporte a mesa digitalizadora com Pointer Events, gravacao com MediaRecorder API e transmissao de tracos em tempo real. Tudo em PHP + JavaScript puro.

Como funciona a videoconferencia: Quando o professor inicia a aula, o sistema cria uma "sala" no banco de dados com um codigo unico. Cada participante que entra gera um ID de sessao e comeca o processo de negociacao WebRTC. A sinalizacao (troca de SDP offer/answer e ICE candidates) e feita via banco MySQL com polling a cada 2 segundos — sem WebSocket, sem servidor de sinalizacao externo.

A lousa digital: Usa Canvas HTML5 com Pointer Events API, que captura pressao, inclinacao e posicao de qualquer dispositivo apontador — mouse, dedo, stylus, Apple Pencil ou mesa digitalizadora Wacom. Os tracos sao transmitidos em tempo real para todos os participantes via banco de dados.

Gravacao automatica: O navegador do professor usa MediaRecorder API para gravar o stream de video/audio. Ao encerrar a aula, o arquivo e enviado para Cloudflare R2 (compativel com S3) e associado automaticamente com a disciplina e assunto corretos.
WebRTC (RTCPeerConnection) Canvas API Pointer Events API MediaRecorder API MySQL Polling PHP JavaScript Vanilla Cloudflare R2
PASSO 1 — Professor inicia a sala (ajax-aula.php)
Backend PHP — Criacao da sala e gerenciamento de presenca
// Iniciar sala de aula ao vivo
if($acao === 'iniciar_sala'){
    $curso_id = intval($_POST['curso_id']);
    $codigo   = bin2hex(random_bytes(8)); // codigo unico da sala

    // Criar sala no banco com status 'ao_vivo'
    $s = $conn->prepare("INSERT INTO am_salas
        (tenant_slug, curso_id, codigo, status, professor_id, iniciada_em)
        VALUES (?, ?, ?, 'ao_vivo', ?, NOW())");
    $s->bind_param("ssis", $ue, $curso_id, $codigo, $am_uid);
    $s->execute();
    $sala_id = $conn->insert_id;

    // Registrar presenca do professor
    $sessao = session_id();
    $conn->prepare("INSERT INTO am_sala_presenca
        (sala_id, tenant_slug, usuario_id, usuario_nome,
         usuario_tipo, sessao_id, ultimo_ping)
        VALUES (?,?,?,?,'professor',?,NOW())")
        ->execute([$sala_id,$ue,$am_uid,$am_nome,$sessao]);

    resp(['ok'=>true, 'sala_id'=>$sala_id, 'codigo'=>$codigo]);
}

// Ping de presenca — chamado a cada 5 segundos pelo cliente
if($acao === 'ping'){
    $sessao = session_id();
    $conn->query("UPDATE am_sala_presenca
        SET ultimo_ping=NOW()
        WHERE sessao_id='$sessao'");

    // Detectar participantes que saíram (sem ping ha mais de 15s)
    $conn->query("DELETE FROM am_sala_presenca
        WHERE sala_id=$sala_id
        AND ultimo_ping < DATE_SUB(NOW(), INTERVAL 15 SECOND)");

    // Retornar lista atualizada de participantes
    $participantes = $conn->query("SELECT usuario_nome, usuario_tipo,
        mic_ativo, cam_ativo FROM am_sala_presenca
        WHERE sala_id=$sala_id")->fetch_all(MYSQLI_ASSOC);

    resp(['ok'=>true, 'participantes'=>$participantes]);
}
PASSO 2 — Sinalizacao WebRTC via banco (sem WebSocket)
Troca de SDP e ICE candidates via MySQL polling
// Enviar sinal WebRTC (offer, answer ou ICE candidate)
if($acao === 'sinal_webrtc'){
    $de_sessao   = session_id();
    $para_sessao = $_POST['para_sessao'];
    $tipo        = $_POST['tipo'];    // 'offer' | 'answer' | 'candidate'
    $payload     = $_POST['payload']; // JSON com SDP ou ICE candidate

    $s = $conn->prepare("INSERT INTO am_sala_webrtc
        (sala_id, tenant_slug, de_sessao, para_sessao, tipo, payload)
        VALUES (?,?,?,?,?,?)");
    $s->bind_param("isssss",
        $sala_id, $ue, $de_sessao, $para_sessao, $tipo, $payload);
    $s->execute();
    resp(['ok'=>true]);
}

// Buscar sinais pendentes (chamado a cada 2 segundos pelo JS)
if($acao === 'buscar_sinais'){
    $sessao = session_id();
    $s = $conn->prepare("SELECT id, de_sessao, tipo, payload
        FROM am_sala_webrtc
        WHERE para_sessao=? AND lido=0
        ORDER BY id ASC LIMIT 50");
    $s->bind_param("s", $sessao);
    $s->execute();
    $sinais = $s->get_result()->fetch_all(MYSQLI_ASSOC);

    // Marcar como lidos para nao repetir
    if(!empty($sinais)){
        $ids = implode(',', array_column($sinais, 'id'));
        $conn->query("UPDATE am_sala_webrtc SET lido=1 WHERE id IN ($ids)");
    }
    resp(['ok'=>true, 'sinais'=>$sinais]);
}
PASSO 3 — JavaScript: estabelecer conexao WebRTC
RTCPeerConnection — negociacao e streams de video
// Configuracao dos servidores STUN (para atravessar NAT/firewall)
var ICE_SERVERS = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        { urls: 'stun:stun1.l.google.com:19302' }
    ]
};

// Criar conexao com um participante especifico
function criarConexao(sessao_remota) {
    var pc = new RTCPeerConnection(ICE_SERVERS);
    conexoes[sessao_remota] = pc;

    // Adicionar stream local (camera + microfone)
    streamLocal.getTracks().forEach(function(track){
        pc.addTrack(track, streamLocal);
    });

    // Quando receber stream remoto, exibir no video tile
    pc.ontrack = function(event){
        exibirVideoParticipante(sessao_remota, event.streams[0]);
    };

    // Quando gerar ICE candidate, enviar para o outro lado via banco
    pc.onicecandidate = function(event){
        if(event.candidate){
            enviarSinal(sessao_remota, 'candidate',
                JSON.stringify(event.candidate));
        }
    };
    return pc;
}

// Processar sinais recebidos do banco (polling a cada 2s)
async function processarSinais(sinais){
    for(var sinal of sinais){
        var pc = conexoes[sinal.de_sessao]
               || criarConexao(sinal.de_sessao);
        var payload = JSON.parse(sinal.payload);

        if(sinal.tipo === 'offer'){
            await pc.setRemoteDescription(payload);
            var answer = await pc.createAnswer();
            await pc.setLocalDescription(answer);
            enviarSinal(sinal.de_sessao, 'answer',
                JSON.stringify(answer));
        }
        else if(sinal.tipo === 'answer'){
            await pc.setRemoteDescription(payload);
        }
        else if(sinal.tipo === 'candidate'){
            await pc.addIceCandidate(payload);
        }
    }
}
PASSO 4 — Lousa digital com suporte a mesa digitalizadora
Canvas + Pointer Events API — pressao, inclinacao, stylus
var canvas = document.getElementById('canvas-lousa');
var ctx    = canvas.getContext('2d');
var desenhando = false;

// Pointer Events captura mouse, dedo, stylus e mesa digitalizadora
canvas.addEventListener('pointerdown', function(e){
    e.preventDefault();
    desenhando = true;
    ctx.beginPath();
    var pos = getPos(e);
    ctx.moveTo(pos.x, pos.y);

    // Suporte a pressao do stylus (0.0 a 1.0)
    // Mesa Wacom, Apple Pencil, Surface Pen
    var pressao = e.pressure || 0.5;
    ctx.lineWidth = pressao * espessura_atual * 2;
    ctx.strokeStyle = cor_atual;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
});

canvas.addEventListener('pointermove', function(e){
    if(!desenhando) return;
    e.preventDefault();
    var pos = getPos(e);

    // Ajustar espessura dinamicamente pela pressao
    if(e.pressure > 0){
        ctx.lineWidth = e.pressure * espessura_atual * 2;
    }

    ctx.lineTo(pos.x, pos.y);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(pos.x, pos.y);

    // Transmitir traco para todos os participantes
    transmitirTraco({
        x: pos.x / canvas.width,   // normalizar para 0-1
        y: pos.y / canvas.height,  // independente de resolucao
        pressao: e.pressure,
        cor: cor_atual,
        espessura: espessura_atual,
        tipo: 'move'
    });
});

// Receber tracos de outros participantes
function receberTraco(dados){
    var x = dados.x * canvas.width;
    var y = dados.y * canvas.height;
    ctx.strokeStyle = dados.cor;
    ctx.lineWidth   = dados.pressao * dados.espessura * 2;
    if(dados.tipo === 'start') ctx.beginPath(), ctx.moveTo(x,y);
    else { ctx.lineTo(x,y); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x,y); }
}
PASSO 5 — Gravacao automatica com MediaRecorder
Gravar aula e enviar para Cloudflare R2
var mediaRecorder, chunks = [];

// Iniciar gravacao ao comecar a aula
function iniciarGravacao(stream){
    var opcoes = { mimeType: 'video/webm;codecs=vp9,opus' };
    if(!MediaRecorder.isTypeSupported(opcoes.mimeType)){
        opcoes = { mimeType: 'video/webm' }; // fallback
    }

    mediaRecorder = new MediaRecorder(stream, opcoes);

    // Coletar chunks a cada 5 segundos
    mediaRecorder.ondataavailable = function(e){
        if(e.data.size > 0) chunks.push(e.data);
    };

    // Ao parar, montar o blob e enviar
    mediaRecorder.onstop = function(){
        var blob = new Blob(chunks, { type: 'video/webm' });
        enviarGravacao(blob);
        chunks = [];
    };

    mediaRecorder.start(5000); // chunk a cada 5s
}

// Enviar gravacao para o servidor (que salva no R2)
async function enviarGravacao(blob){
    var fd = new FormData();
    fd.append('gravacao', blob, 'aula_' + Date.now() + '.webm');
    fd.append('sala_id', SALA_ID);
    fd.append('curso_id', CURSO_ID);

    var r = await fetch('/aula-ao-vivo/ajax-aula.php?acao=salvar_gravacao',
        { method: 'POST', body: fd });
    var d = await r.json();
    if(d.ok) console.log('Gravacao salva:', d.url);
}
Simulados e Flashcards com IA
Groq API + LLaMA 3.3 70B — sem alucinacoes
Como funciona: O professor cria resumos no editor. Quando clica em "Gerar Simulado" ou "Gerar Flashcards", o sistema extrai o texto puro do HTML dos resumos, monta um prompt rigoroso com instrucoes anti-alucinacao e envia para a API do Groq com o modelo LLaMA 3.3 70B. A temperatura e definida em 0.3 (baixa) para minimizar criatividade e maximizar fidelidade ao texto original.

Por que nao erra: O prompt proibe explicitamente o uso de conhecimento externo. A IA so pode usar o texto fornecido. Alem disso, o JSON retornado e validado campo a campo antes de ser exibido — questoes sem enunciado ou sem resposta correta sao descartadas automaticamente.

Selecao de topicos: O professor pode selecionar topicos especificos (H2/H3 dos resumos) para gerar questoes apenas sobre aquele conteudo. O sistema extrai apenas as secoes selecionadas do HTML antes de enviar para a IA.
Groq APILLaMA 3.3 70BPHP cURLJSON ValidationTemperatura 0.3
Prompt anti-alucinacao + chamada Groq
ajax-flashcards.php — logica completa de geracao
// Extrair texto limpo dos resumos HTML
foreach($resumos as $res){
    $html = $res['conteudo_html'];
    // Converter headings em texto estruturado
    $html = preg_replace('/]*>(.*?)<\/h2>/i', "\n\n## $1\n", $html);
    $html = preg_replace('/]*>(.*?)<\/h3>/i', "\n\n### $1\n", $html);
    $html = preg_replace('/]*>/i', "\n- ", $html);
    $html = preg_replace('//i', "\n", $html);
    $t = strip_tags($html);
    $t = html_entity_decode($t, ENT_QUOTES|ENT_HTML5, 'UTF-8');
    $t = preg_replace('/[ \t]+/', ' ', $t);
    $texto .= "\n\n=== " . $res['titulo'] . " ===\n" . trim($t);
}
$texto = mb_substr(trim($texto), 0, 9000); // limite de tokens

// Prompt com regras absolutas contra alucinacao
$prompt = "Voce e um organizador de conteudo didatico.
Sua unica funcao e transformar o texto abaixo em questoes.

REGRAS ABSOLUTAS — NUNCA VIOLE:
1. Use EXCLUSIVAMENTE as informacoes presentes no texto.
   ZERO conhecimento externo.
2. Nao invente, nao complete, nao infira nada que nao
   esteja explicitamente escrito no texto.
3. Para V/F: so afirme como Verdadeiro o que o texto
   confirma explicitamente.
4. Gere exatamente $qtd questoes.
5. Retorne SOMENTE o array JSON, sem texto antes ou depois.

FORMATO JSON:
[
  {\"tipo\":\"multipla\",\"questao\":\"...\",
   \"resposta_correta\":\"texto da opcao correta\",
   \"opcao_b\":\"...\",\"opcao_c\":\"...\",\"opcao_d\":\"...\"},
  {\"tipo\":\"vf\",\"questao\":\"...\",
   \"resposta_correta\":\"Verdadeiro\",
   \"opcao_b\":\"Falso\",\"opcao_c\":null,\"opcao_d\":null}
]

TEXTO DO RESUMO (use SOMENTE este conteudo):
$texto";

// Chamar Groq com temperatura baixa
$ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode([
        'model'       => 'llama-3.3-70b-versatile',
        'messages'    => [['role'=>'user','content'=>$prompt]],
        'temperature' => 0.3,   // baixo = menos criativo = menos erro
        'max_tokens'  => 5000
    ], JSON_UNESCAPED_UNICODE),
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $groq_key
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 90,
]);
$resp = curl_exec($ch);
$dados = json_decode($resp, true);
$conteudo = $dados['choices'][0]['message']['content'];

// Limpar markdown e extrair JSON
$conteudo = preg_replace('/^```(?:json)?\s*/i', '', trim($conteudo));
$conteudo = preg_replace('/\s*```$/', '', trim($conteudo));
if(preg_match('/(\[[\s\S]*\])/s', $conteudo, $m)) $conteudo = $m[1];

// Validar cada questao antes de retornar
$questoes_raw = json_decode($conteudo, true);
foreach($questoes_raw as $q){
    // Descartar questoes incompletas
    if(empty($q['questao']) || empty($q['resposta_correta'])) continue;
    $questoes[] = $q;
}
Matricula Digital com Contrato e Assinatura
Fluxo completo: dados, contrato, selfie, assinatura, hash SHA-256
O fluxo: O professor configura um contrato com variaveis dinamicas (nome, CPF, valor, datas). Ao compartilhar o link da turma, o aluno preenche seus dados, le o contrato com os dados ja preenchidos, tira uma selfie pela camera do celular e assina digitalmente com o dedo no canvas. Tudo e salvo com um hash SHA-256 para validacao futura.

Validade juridica: O sistema captura IP de origem, user-agent, data/hora, selfie biometrica e assinatura manuscrita digitalizada. O hash SHA-256 garante que o contrato nao foi alterado apos a assinatura. Qualquer pessoa pode verificar a autenticidade em /contrato-view.php?modo=validar.
PHPCanvas APISHA-256Base64MediaDevices APIMySQL
Geracao do hash e salvamento do contrato
ajax-matricula.php — contrato + hash + selfie + assinatura
// Preencher variaveis dinamicas do contrato
$contrato = str_replace([
    '{{nome_aluno}}',    '{{cpf_aluno}}',
    '{{endereco_aluno}}','{{telefone_aluno}}',
    '{{nome_turma}}',    '{{nome_curso}}',
    '{{valor_parcela}}', '{{total_parcelas}}',
    '{{data_matricula}}','{{data_fim_contrato}}'
], [
    htmlspecialchars($nome), htmlspecialchars($cpf_fmt),
    htmlspecialchars($end),  htmlspecialchars($tel),
    htmlspecialchars($turma['nome']),
    htmlspecialchars($turma['curso_nome']),
    $valor_fmt, $total_parc,
    date('d/m/Y'), $data_fim
], $turma['contrato_template']);

// Hash SHA-256 do conteudo + dados do aluno + timestamp
// Garante que o contrato nao pode ser alterado apos assinatura
$hash = hash('sha256',
    $contrato . $nome . $cpf . $end . $tel . date('Y-m-d H:i:s')
);

// Token unico para o aluno acessar seu painel
$token_aluno = bin2hex(random_bytes(32));

// Salvar tudo — selfie e assinatura em base64 no banco
// (para contratos pequenos; grandes vao para storage)
$sql = "INSERT INTO am_matriculas_ext
    (tenant_slug, turma_id, token_aluno, nome, cpf,
     endereco, telefone, email, ip_matricula, user_agent,
     selfie_base64, assinatura_base64,
     contrato_html, contrato_hash, status_aprovacao)
    VALUES (...)";
$conn->query($sql);

// Gerar parcelas automaticamente
gerarParcelas($conn, $tenant, $matricula_id, $turma);

// Notificar professor por email
mail($prof['email'],
    "Nova matricula aguardando aprovacao — $nome",
    "Aluno: $nome\nCPF: $cpf_fmt\nTurma: {$turma['nome']}"
);
Captura de selfie via camera (JavaScript)
matricula/index.php — MediaDevices + Canvas
// Acessar camera frontal do dispositivo
async function iniciarCamera(){
    try {
        _stream = await navigator.mediaDevices.getUserMedia({
            video: { facingMode: 'user' }, // camera frontal
            audio: false
        });
        video.srcObject = _stream;
    } catch(e){
        // Fallback: mostrar botao de upload de arquivo
        mostrarUpload();
    }
}

// Capturar frame do video como imagem base64
function tirarSelfie(){
    var canvas = document.getElementById('canvas-selfie');
    var ctx = canvas.getContext('2d');
    canvas.width  = video.videoWidth  || 400;
    canvas.height = video.videoHeight || 300;
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
    // Converter para JPEG base64 (qualidade 80%)
    _selfieB64 = canvas.toDataURL('image/jpeg', 0.8);
}

// Assinatura digital com Canvas
canvas.addEventListener('touchmove', function(e){
    e.preventDefault(); // evitar scroll durante assinatura
    var touch = e.touches[0];
    var rect  = canvas.getBoundingClientRect();
    var x = (touch.clientX - rect.left) * (canvas.width / rect.width);
    var y = (touch.clientY - rect.top)  * (canvas.height / rect.height);
    ctx.lineTo(x, y);
    ctx.stroke();
    _assinHasData = true;
}, { passive: false });
Financeiro Automatizado
Parcelas, PIX, controle de inadimplencia, cobranca por email
Como funciona: O professor configura o financeiro da turma: modalidade (mensal, anual, diaria), valor, dia de vencimento e metodo de pagamento (PIX ou link de cartao). No momento da matricula, o sistema gera automaticamente todas as parcelas com suas datas de vencimento. O sistema detecta inadimplencia comparando a data de vencimento com a data atual.

Cobranca automatica: Um cron job (ou chamada manual) percorre as parcelas vencidas e envia emails de cobranca para os alunos inadimplentes com as instrucoes de pagamento configuradas pelo professor.
PHPMySQLDateTimemail()
Geracao automatica de parcelas
ajax-matricula.php — funcao gerarParcelas()
function gerarParcelas($conn, $tenant, $matricula_id, $turma){
    $modalidade = $turma['fin_modalidade']; // mensal|anual|diaria
    $duracao    = intval($turma['fin_duracao']   ?? 1);
    $valor      = floatval($turma['fin_valor']   ?? 0);
    $vencimento = $turma['fin_vencimento']; // dia_matricula|dia_fixo
    $dia_fixo   = intval($turma['fin_dia_fixo']  ?? 1);
    $inicio     = $turma['fin_inicio']; // imediato|proximo_mes

    // Anual = 1 parcela; outros = duracao parcelas
    $n = ($modalidade === 'anual') ? 1 : $duracao;
    $data_mat = new DateTime();

    for($i = 1; $i <= $n; $i++){
        $dt = clone $data_mat;

        if($vencimento === 'dia_matricula'){
            // Vencimento no mesmo dia, mes seguinte
            if($modalidade === 'diaria')
                $dt->modify('+' . ($i-1) . ' days');
            else
                $dt->modify('+' . ($i-1) . ' months');
        } else {
            // Vencimento em dia fixo do mes
            if($inicio === 'proximo_mes') $dt->modify('+1 month');
            $dt->modify('+' . ($i-1) . ' months');
            // Limitar a 28 para evitar problemas em fevereiro
            $dt->setDate(
                (int)$dt->format('Y'),
                (int)$dt->format('m'),
                min($dia_fixo, 28)
            );
        }

        $dv = $dt->format('Y-m-d');
        $ins = $conn->prepare("INSERT INTO am_parcelas
            (tenant_slug, matricula_id, turma_id,
             numero_parcela, valor, data_vencimento)
            VALUES (?,?,?,?,?,?)");
        $ins->bind_param("siiids",
            $tenant, $matricula_id, $turma['id'], $i, $valor, $dv);
        $ins->execute();
    }
}

// Detectar inadimplencia (chamado ao listar parcelas)
$hoje = date('Y-m-d');
$conn->query("UPDATE am_parcelas
    SET status = 'atrasado'
    WHERE status = 'pendente'
    AND data_vencimento < '$hoje'
    AND matricula_id = $matricula_id");
Banco de Dados Multi-tenant
MySQL com isolamento por tenant_slug — um banco para todos
Arquitetura multi-tenant: Em vez de criar um banco separado por cliente (caro e complexo de gerenciar), todos os dados ficam no mesmo banco MySQL. Cada tabela tem um campo tenant_slug que identifica a qual escola/professor aquele registro pertence. Toda query filtra pelo slug da sessao logada.

Isolamento garantido: O campo tenant_slug e sempre validado na sessao PHP antes de qualquer operacao. Um professor nao consegue ver dados de outro professor — o sistema filtra automaticamente em todas as queries.

Sinalizacao WebRTC no banco: A tabela am_sala_webrtc funciona como fila de mensagens para a sinalizacao WebRTC. Cada participante faz polling a cada 2 segundos buscando sinais pendentes para sua sessao.
MySQL 8InnoDButf8mb4Multi-tenantIndexes
Estrutura das tabelas principais
banco.sql — tabelas principais (simplificado)
-- Tenants (cada escola/professor e um tenant)
CREATE TABLE am_tenants (
    id        INT AUTO_INCREMENT PRIMARY KEY,
    slug      VARCHAR(100) NOT NULL UNIQUE, -- ex: "fontelis"
    nome      VARCHAR(255) NOT NULL,
    email     VARCHAR(255) NOT NULL,
    plano     ENUM('inicial','turma','escola','academia','pro'),
    template  VARCHAR(50)  DEFAULT 'netflix',
    cor       VARCHAR(20)  DEFAULT '#CC0000',
    ativo     TINYINT(1)   DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Cursos — cada um tem um slug publico (aulameet.com/{slug})
CREATE TABLE am_cursos (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    tenant_slug VARCHAR(100) NOT NULL,  -- isolamento por escola
    nome        VARCHAR(500) NOT NULL,
    slug        VARCHAR(100) NULL UNIQUE, -- URL publica
    tipo        ENUM('curso','mentoria') DEFAULT 'curso',
    status      ENUM('rascunho','ativo','arquivado'),
    INDEX idx_tenant (tenant_slug),
    INDEX idx_slug   (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Sinalizacao WebRTC (fila de mensagens para peer-to-peer)
CREATE TABLE am_sala_webrtc (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    sala_id     INT NOT NULL,
    de_sessao   VARCHAR(64) NOT NULL,
    para_sessao VARCHAR(64) NOT NULL,
    tipo        VARCHAR(30) NOT NULL,  -- offer|answer|candidate
    payload     MEDIUMTEXT  NOT NULL,  -- JSON com SDP ou ICE
    lido        TINYINT(1)  DEFAULT 0,
    criado_em   DATETIME    DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_para    (para_sessao, lido), -- index para polling rapido
    INDEX idx_criado  (criado_em)          -- para limpeza automatica
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Matriculas com contrato digital completo
CREATE TABLE am_matriculas_ext (
    id                INT AUTO_INCREMENT PRIMARY KEY,
    tenant_slug       VARCHAR(100) NOT NULL,
    turma_id          INT NOT NULL,
    token_aluno       VARCHAR(64)  NOT NULL UNIQUE,
    nome              VARCHAR(255) NOT NULL,
    cpf               VARCHAR(14)  NOT NULL,
    selfie_base64     LONGTEXT     DEFAULT NULL, -- foto do aluno
    assinatura_base64 LONGTEXT     DEFAULT NULL, -- assinatura canvas
    contrato_html     LONGTEXT     DEFAULT NULL, -- contrato preenchido
    contrato_hash     VARCHAR(64)  DEFAULT NULL, -- SHA-256 do contrato
    ip_matricula      VARCHAR(45)  DEFAULT NULL,
    status_aprovacao  ENUM('pendente','aprovado','rejeitado'),
    INDEX idx_tenant (tenant_slug),
    INDEX idx_turma  (turma_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Site Builder + Subdominios Automaticos
seucurso.aulameet.com criado automaticamente via API Hostinger
Como funciona: Ao criar um curso com slug, o sistema chama a API da Hostinger para criar o subdominio automaticamente. O .htaccess captura qualquer requisicao de subdominio e redireciona para um unico arquivo PHP que identifica o tenant pelo HOST da requisicao e renderiza o template correto (Netflix, Moderno, Elegante, Bold ou Glassmorphism).

5 templates prontos: Cada template e um arquivo PHP independente que recebe as variaveis $tenant, $cursos e $professor e renderiza a pagina completa. O professor pode personalizar cores e conteudo pelo editor visual.
Apache .htaccessPHPHostinger APIMySQL
Roteamento de subdominios e slugs
.htaccess — roteamento inteligente
# Subdominios dinamicos: meupreparatorio.aulameet.com
# Captura qualquer subdominio e envia para subdominio.php
RewriteCond %{HTTP_HOST} ^(?!www\.)([a-z0-9\-]+)\.aulameet\.com$ [NC]
RewriteCond %{REQUEST_URI} !^/acesso/subdominio\.php
RewriteCond %{REQUEST_URI} !^/uploads/
RewriteRule ^(.*)$ /acesso/subdominio.php [QSA,L]

# Paginas publicas de cursos: aulameet.com/{slug}
# So roteia se nao for arquivo ou pasta real
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([a-z0-9][a-z0-9\-]{1,60})/?$ /curso-publico.php?slug=$1 [QSA,L]
Identificacao do tenant pelo HOST
acesso/subdominio.php — multi-tenant por HOST
// Extrair o slug do subdominio a partir do HOST
// Ex: "fontelis.aulameet.com" -> slug = "fontelis"
preg_match('/^([a-z0-9\-]+)\.aulameet\.com$/i',
    $_SERVER['HTTP_HOST'], $m);
$slug = $m[1] ?? '';

// Buscar tenant no banco pelo slug
$s = $conn->prepare("SELECT * FROM am_tenants
    WHERE slug=? AND ativo=1 LIMIT 1");
$s->bind_param("s", $slug);
$s->execute();
$tenant = $s->get_result()->fetch_assoc();

if(!$tenant){ http_response_code(404); die('Nao encontrado'); }

// Buscar cursos ativos do tenant
$cursos = buscarCursosAtivos($tenant['slug'], $conn);

// Carregar template escolhido pelo professor
$template = $tenant['template'] ?? 'netflix';
$tpl = __DIR__ . '/templates/template-' . $template . '.php';
include $tpl; // renderiza a pagina completa
Trilha de Etapas Gamificada
Aprendizado sequencial com desbloqueio, ranking e confetes
Como funciona: O professor cria etapas sequenciais para uma turma. Cada etapa tem exercicios com questoes e uma nota minima para aprovacao. O aluno so pode avancar para a proxima etapa apos ser aprovado na anterior. O sistema registra todas as tentativas, calcula a nota e atualiza o ranking da turma em tempo real.

Gamificacao: Ao ser aprovado em uma etapa, o sistema dispara uma animacao de confetes no navegador. O ranking mostra a posicao de cada aluno com medalhas de ouro, prata e bronze. O progresso e exibido em uma barra visual com porcentagem.
PHPMySQLJavaScriptCSS Animations
Logica de desbloqueio e calculo de nota
etapas/ajax-etapas.php — desbloqueio e aprovacao
// Verificar se etapa esta desbloqueada para o aluno
function etapa_desbloqueada($etapa_id, $usuario_id, $conn){
    $s = $conn->prepare("SELECT ordem, turma_id FROM am_etapas
        WHERE id=? LIMIT 1");
    $s->bind_param("i", $etapa_id);
    $s->execute();
    $etapa = $s->get_result()->fetch_assoc();

    // Primeira etapa sempre disponivel
    if($etapa['ordem'] <= 1) return true;

    // Verificar aprovacao na etapa anterior
    $s = $conn->prepare("SELECT id FROM am_etapa_resultados
        WHERE usuario_id=? AND turma_id=? AND etapa_ordem=?
        AND aprovado=1 LIMIT 1");
    $s->bind_param("iii",
        $usuario_id, $etapa['turma_id'], $etapa['ordem'] - 1);
    $s->execute();
    return $s->get_result()->num_rows > 0;
}

// Calcular nota e registrar resultado
if($acao === 'finalizar_etapa'){
    $respostas  = $_POST['respostas']; // array [questao_id => resposta]
    $total      = count($questoes);
    $acertos    = 0;

    foreach($questoes as $q){
        $resp_aluno = $respostas[$q['id']] ?? '';
        if($resp_aluno === $q['resposta_correta']) $acertos++;
    }

    $nota     = ($total > 0) ? round(($acertos / $total) * 10, 1) : 0;
    $aprovado = ($nota >= $etapa['nota_minima']) ? 1 : 0;

    // Salvar resultado (pode ter multiplas tentativas)
    $conn->prepare("INSERT INTO am_etapa_resultados
        (usuario_id, etapa_id, turma_id, etapa_ordem,
         nota, acertos, total, aprovado)
        VALUES (?,?,?,?,?,?,?,?)")
        ->execute([$uid,$etapa_id,$turma_id,
                   $etapa['ordem'],$nota,$acertos,$total,$aprovado]);

    resp(['ok'=>true, 'nota'=>$nota, 'aprovado'=>$aprovado,
          'acertos'=>$acertos, 'total'=>$total]);
}

Decisoes tecnicas que fizeram funcionar

Por que o sistema funciona sem depender de servicos caros e como cada problema foi resolvido de forma simples.

WebRTC sem servidor de midia

A videoconferencia usa conexao peer-to-peer direta. A sinalizacao e feita via banco MySQL com polling a cada 2 segundos. Sem TURN server proprio — usa servidores STUN publicos do Google. Funciona para a maioria das redes sem custo adicional.

IA sem alucinacoes

O segredo e o prompt: a IA recebe o texto do resumo e instrucoes explicitas para nao usar conhecimento externo. Temperatura 0.3 reduz criatividade e aumenta fidelidade. O JSON retornado e validado campo a campo antes de exibir.

Mesa digitalizadora nativa

O Canvas HTML5 com Pointer Events API captura pressao, inclinacao e posicao de stylus nativamente. Funciona com qualquer tablet Wacom, iPad com Apple Pencil ou mesa digitalizadora sem instalar drivers ou plugins.

Contrato juridicamente robusto

O contrato captura: dados pessoais, IP de origem, user-agent, selfie biometrica, assinatura manuscrita digitalizada e hash SHA-256 do conteudo. Tudo armazenado e verificavel publicamente via URL de validacao.

Subdominios automaticos

Ao criar um curso, a API da Hostinger e chamada para criar o subdominio. O .htaccess captura qualquer requisicao de subdominio e redireciona para um unico PHP que identifica o tenant pelo HOST e renderiza o template.

Multi-tenant simples e eficiente

Em vez de criar um banco por cliente, todos os dados ficam no mesmo banco MySQL separados por tenant_slug. Cada query filtra pelo slug da sessao. Simples, eficiente e escalavel sem custo adicional de infraestrutura.

💛 Mantenha o sistema livre

O AulaMeet e desenvolvido e mantido por uma unica pessoa, no tempo livre, sem financiamento externo. Se o sistema te ajudou, considere fazer uma doacao para manter tudo funcionando e gratuito para todos.

Chave PIX
05294561336
Titular: Jonatas de Aguiar Mota Fontelis
R$ 49,99 R$ 79,99 R$ 99,99 R$ +
Voltar ao inicio