KassinãoDocs
PT EN
GitHub

Bring Kassinão into your Discord.

Install and operate the Discord bot that records calls, transcribes each person, and creates minutes, decisions, and tasks.

Quick start

The shortest path from a new server to your first recorded call.

  1. Create the appCopy the Application ID, bot token, and Client Secret.
  2. Prepare the serverClone the project and fill in the four required variables.
  3. Start DockerFollow the logs until Kassinão reports that it is online.
  4. Record a callJoin a voice channel and use /record.
Terminal
git clone https://github.com/resolvicomai/kassinao.git
cd kassinao
cp .env.example .env && chmod 600 .env
mkdir -p recordings && chmod 700 recordings
.env
DISCORD_TOKEN=paste_the_bot_token
APPLICATION_ID=paste_the_application_id
DISCORD_CLIENT_SECRET=paste_the_client_secret
APP_URL=https://kassinao.your-domain.com
Terminal
docker compose up -d --build
docker compose logs -f
The bot records without AI.

Transcription and minutes are optional. Configure a provider after validating recording, login, and downloads.

Requirements

Kassinão is a persistent voice bot. It must stay connected to Discord.

Required to operate

  • A server or computer with Docker and Docker Compose.
  • An application created in the Discord Developer Portal. No privileged intent is required.
  • A public HTTPS URL for login and downloads in production.
  • A persistent volume for the recordings directory.

Optional

  • Cloudflare Tunnel for HTTPS without opening ports.
  • A transcription and minutes provider key.
  • Node.js 20+ on the computer running the MCP connector.
  • Node.js 22+ only for development outside Docker.
Do not use serverless.

Vercel and Netlify do not keep the voice gateway WebSocket alive. Use Docker on a persistent machine.

Docker installation

Configure Discord first, then the public URL, and only then start the container.

Create the application

In the Discord Developer Portal, create an application. Copy the Application ID, generate the bot token, and copy the OAuth2 Client Secret.

Register exactly APP_URL/auth/callback under OAuth2 Redirects.

Invite the bot

Use the bot and applications.commands scopes. The project permission number is 68242432.

Invite URL
https://discord.com/oauth2/authorize?client_id=YOUR_APP_ID&scope=bot%20applications.commands&permissions=68242432

Permissions: View Channels, Send Messages, Embed Links, Read Message History, Connect, and Change Nickname.

Publish HTTPS

With Cloudflare Tunnel, point the public hostname to kassinao:8080. Set TUNNEL_TOKEN and COMPOSE_PROFILES=tunnel.

Direct IP is for testing only. Discord OAuth accepts HTTP only on localhost.

Start and validate

Start Compose, follow the log, and open /health. Recordings live in the ./recordings volume.

Terminal
docker compose up -d --build
docker compose logs -f

Variables and configuration

Start with the required block. Open the remaining groups only when you need the feature.

Discord and web accessBot identity, OAuth, public URL, and language.
DISCORD_TOKEN
Default: required Bot token created in the Discord Developer Portal.
APPLICATION_ID
Default: required Application ID used to register commands.
DISCORD_CLIENT_SECRET
Default: required OAuth Client Secret used for private-page login.
APP_URL
Default: BASE_URL or http://localhost:8080 Private origin for the app, OAuth, recordings, and downloads. Register APP_URL/auth/callback as a Discord redirect.
BASE_URL
Default: empty Backward-compatible alias for APP_URL. New installations should prefer APP_URL.
PUBLIC_URL
Default: APP_URL Origin for the landing page and public demo. Keep it equal to the app when using one domain.
DOCS_URL
Default: PUBLIC_URL Documentation origin. When separate, Portuguese lives at / and English at /en.
MCP_URL
Default: APP_URL Public origin for the MCP API. The connector uses this value as KASSINAO_URL.
GUILD_ID
Default: empty Optional. Limits immediate command registration to one server.
PORT
Default: 8080 Internal HTTP port for the Express server.
TUNNEL_TOKEN
Default: empty Cloudflare Tunnel token. Also enable the tunnel profile.
COMPOSE_PROFILES
Default: empty Use tunnel to start Cloudflare Tunnel alongside the bot.
COOKIE_SECRET
Default: generated and persisted Session secret with at least 32 bytes. When empty, the bot creates one in the volume.
REPO_PUBLIC
Default: false Enables public repository links in the interface.
DEFAULT_LOCALE
Default: en Fallback language when Discord does not provide a locale.
TZ
Default: America/Sao_Paulo Fallback timezone for dates. On the web, the browser takes priority.
Recording, retention, and diskFiles, duration, quality, expiration, and operational guards.
RECORDINGS_DIR
Default: ./recordings Persistent recordings directory. Docker uses /app/recordings.
RETENTION_DAYS
Default: 7 Days until audio expires. Zero disables all automatic expiration.
TEXT_RETENTION_DAYS
Default: 90 Retention for transcript, minutes, and notes. Never lower than audio retention.
MAX_RECORDING_HOURS
Default: 6 Maximum duration of each recording.
MANUAL_RECORD_USER_COOLDOWN_SEC
Default: 60 Global cooldown per regular member between manual starts. Admins bypass it.
MANUAL_RECORD_GUILD_COOLDOWN_SEC
Default: 15 Server cooldown between manual starts by regular members.
MANUAL_RECORD_GUILD_STARTS_PER_24H
Default: 48 Rolling 24-hour cap for manual starts per server. Admins do not consume the quota.
MP3_BITRATE
Default: 192k Bitrate for individual MP3 files and the mix.
MIN_FREE_MB_START
Default: 500 Minimum free space required to start a recording.
MIN_FREE_MB_ABORT
Default: 150 Free-space threshold that triggers a safe stop during recording.
DISK_ALERT_PCT
Default: 85 Usage percentage that sends a DM alert to OWNER_IDS.
Transcription and minutesSpeech provider, vocabulary, local model, and minutes generation.
TRANSCRIBE_PROVIDER
Default: none none, assemblyai, openai, groq, gemini, or command.
TRANSCRIBE_MODEL
Default: provider default Overrides the selected provider model.
TRANSCRIBE_LANGUAGE
Default: pt Language spoken in calls.
TRANSCRIBE_PROMPT
Default: neutral pt-BR context Names, vocabulary, and style context for ASR.
TRANSCRIBE_KEYTERMS
Default: empty Comma-separated fixed vocabulary for AssemblyAI Universal-3.5-Pro.
ASSEMBLYAI_API_KEY / OPENAI_API_KEY / GROQ_API_KEY / GEMINI_API_KEY
Default: empty Set only the keys for providers you use.
TRANSCRIBE_COMMAND
Default: empty Local command with {input} and {output} placeholders.
TRANSCRIBE_TIMEOUT_FACTOR
Default: 5 Timeout multiplier for the local transcriber.
WHISPER_MODEL
Default: small Model used by the local faster-whisper wrapper.
MINUTES_ENABLED
Default: auto auto, true, or false. Auto enables with an OpenRouter or Groq key.
MINUTES_PROVIDER / MINUTES_MODEL
Default: openrouter or groq Provider and model used for summaries, decisions, and tasks.
OPENROUTER_API_KEY
Default: empty Key for minutes through OpenRouter.
MINUTES_MAX_TOKENS
Default: 8192 Maximum output tokens for minutes.
MINUTES_WEBHOOK_URL
Default: empty Env-only webhook. Receives minutes.ready when minutes are ready.
MCP connectorDeliberate activation, allowlist, and token lifetimes.
MCP_SECRET
Default: disabled Dedicated secret with at least 32 bytes. Enables the API and connector.
OWNER_IDS
Default: empty Discord IDs allowed to use /mcp and receive disk alerts.
MCP_ACCESS_TTL_MIN
Default: 15 Short-lived access token duration in minutes.
MCP_REFRESH_TTL_DAYS
Default: 30 Rotating refresh-token duration in days.
Secrets do not belong in Git.

The .env file is already ignored. Generate COOKIE_SECRET and MCP_SECRET with openssl rand -hex 32 and never reuse the same value.

Commands

Discord automatically shows Portuguese or English names based on the client language.

/record [channel]

Joins your voice channel and starts a recording with one separate track per person. Admins can target another visible channel.

Access: Any member in their own channel
/stop

Ends the recording, provides the private link, and starts the transcript and minutes queue.

Access: Starter, call participants, or current admins
/note <text>

Saves a note at the current second. The panel also includes actions to mark a moment or write a note.

Access: Starter, call participants, or current admins
/status

Shows the current recording state when you have permission to follow it.

Access: Server member with access
/recordings

Lists accessible recordings and opens the private workspace with search across transcripts, minutes, and notes.

Access: Results filtered by access
/ask <question> [days]

Searches by topic, person, call date, or deadline and replies only to you with evidence and links to the exact second.

Access: Only meetings you are allowed to open
/autorecord on|off|view

Configures automatic recording per channel and the minimum number of people required to start.

Access: Manage Server
/config minutes-channel|view

Chooses the channel for the generic processing notice or displays the current configuration. Details and links stay in authorized DMs.

Access: Manage Server
/mcp new|revoke-all

Generates a connection code or revokes connectors. It only appears when MCP is enabled. Regular members use the connection page.

Access: Only IDs listed in OWNER_IDS
/help

Opens the interactive bot guide for recording, downloads, questions, privacy, and auto-record.

Access: Any member
/about

Shows the author, AGPL-3.0 license, and source code.

Access: Any member
Use commands inside the server.

That is where the bot can validate server, channel, and permissions. Replies from /ask are ephemeral and only visible to the person who asked.

Recording flow

From the voice-channel command to the private workspace.

  1. 1

    The notice appears before audio starts

    The bot joins the channel, posts the panel, and adds [RECORDING] to its nickname. Capture starts only after the notice.

  2. 2

    Each person gets a track

    Opus packets are decoded to PCM and one ffmpeg process per person records continuous synchronized FLAC. No diarization guesses the speaker.

  3. 3

    Notes preserve the exact second

    Use /note or the panel buttons. Marks appear on the page, in the transcript, and in Audacity labels.

  4. 4

    The recording ends safely

    Use /stop. The bot also ends when the channel empties, reaches the limit, or is disconnected. Extended silence triggers a warning, not a stop.

  5. 5

    Audio becomes available first

    The preprocessed mix powers the player immediately. MP3, FLAC, mix, and Audacity projects are generated on demand and cached.

  6. 6

    Transcript and minutes enter the queue

    VAD normally sends only speech segments; if detection fails, fixed chunks keep the call from being lost. Then minutes create a summary, decisions, and tasks.

Transcription and AI

Recording works without AI. When enabled, AI runs after the call and never decides who spoke.

AssemblyAIUniversal-3.5-Pro, keyterms, and automatic Groq fallback when configured.
GroqWhisper Large V3. Useful for starting with a free tier. Enable Zero Data Retention.
OpenAIWhisper with segments and timestamps.
GeminiAudio through Gemini. Review the retention policy for your tier.
Local commandfaster-whisper, whisper.cpp, or another command that outputs the expected JSON.

Fully local transcription

Build the image with LOCAL_TRANSCRIBE=1 and use the included wrapper. The command must write a JSON array with start, end, and text to {output}.

.env
TRANSCRIBE_PROVIDER=command
TRANSCRIBE_COMMAND=python3 ./scripts/transcribe-local.py {input} {output}
WHISPER_MODEL=small
Terminal
docker compose build --build-arg LOCAL_TRANSCRIBE=1
docker compose up -d

Privacy and permissions

Voice is personal data. Server-side access control applies to every open, search, and connection.

Visible consent

The bot joins the channel, posts a panel, and changes its nickname to [RECORDING] before capturing audio.

Revalidated access

The page requires Discord OAuth and current server membership. Leaving the server ends access.

Recording history

In every channel, only call participants, including muted participants, the starter, or a current admin can open it. Later permission does not unlock the past.

Fails closed

If Discord cannot confirm access, the page denies it. The MCP API returns a temporary error when the check is unavailable.

Tiered retention

Audio can expire before transcripts, minutes, and notes. The operator can also delete only audio or delete everything.

Isolated secrets

Cookies and MCP use different secrets. Rotating MCP_SECRET revokes every connector without changing access rules.

Rotate any exposed credential immediately.

Replace DISCORD_TOKEN, DISCORD_CLIENT_SECRET, TUNNEL_TOKEN, and API keys. Security issues must be reported privately.

MCP connector

Bring meeting memory to Claude, Cursor, or another MCP client without copying the archive to the machine.

MCP is opt-in and read-only.

It does not serve audio, delete recordings, or widen permissions. Every request goes through the same web access check.

Enable on the server

Set a dedicated MCP_SECRET with at least 32 bytes and restart. The /app/conectar-ia page and API exist only when this secret is present.

Terminal
openssl rand -hex 32

Connect each person

Open /app/conectar-ia, sign in with Discord, and create a named connection. Copy the one-time code and run the displayed command: it asks for the code with hidden input, saves the token in a protected local file (0600 on macOS/Linux; inherited profile ACL on Windows), and prints a secret-free config. The computer needs Node.js 20 or newer.

JSON
{
  "mcpServers": {
    "kassinao": {
      "command": "npx",
      "args": ["-y", "kassinao-mcp@1.0.5"],
      "env": {
        "KASSINAO_URL": "https://YOUR-KASSINAO",
        "KASSINAO_PROFILE": "PROFILE_PRINTED_BY_THE_COMMAND"
      }
    }
  }
}

The same flow works without a browser: an ID listed in OWNER_IDS generates a code with /mcp new and exchanges it in the terminal.

Terminal
npx -y kassinao-mcp@1.0.5 exchange --stdin --url https://YOUR-KASSINAO

Available tools

list_meetings

Lists meetings in a time range.

pending_actions

Combines pending actions and deadlines.

search_meetings

Searches transcripts, minutes, and notes.

who_said

Finds what someone said about a topic.

get_meeting

Opens a complete meeting dossier.

Tokens and revocation

The refresh token lives under ~/.config/kassinao-mcp, protected by mode 0600 on macOS/Linux and by the inherited profile ACLs on Windows, and rotates on renewal. Revoke one connection on the page, use /mcp revoke-all, or rotate MCP_SECRET to revoke everyone.

Troubleshooting

Always start with docker compose logs -f. The bot validates configuration on boot and explains invalid variables.

The container does not stay online

Confirm DISCORD_TOKEN, APPLICATION_ID, and DISCORD_CLIENT_SECRET. Also verify that APP_URL is an HTTP or HTTPS origin without a path, query, or hash.

docker compose logs --tail=200 kassinao

Commands do not appear

Confirm the invite included applications.commands and that the bot is already in the server. Restart the bot. If GUILD_ID is set, commands are registered only in that server.

Discord login returns an error

Register exactly APP_URL/auth/callback under OAuth2 Redirects. In production, APP_URL must use HTTPS. After changing the origin, update the redirect and restart.

Cloudflare Tunnel does not start

The service is in a profile. Set COMPOSE_PROFILES=tunnel or run docker compose --profile tunnel up -d. In Cloudflare, the internal target is kassinao:8080.

The recording exists, but there is no transcript

Confirm TRANSCRIBE_PROVIDER is not none and that its provider key exists. Check queue logs. Recording and downloads remain valid without AI.

The transcript is ready, but minutes are not

With MINUTES_ENABLED=auto, minutes only enable when OPENROUTER_API_KEY or GROQ_API_KEY is set. Also verify MINUTES_PROVIDER and provider limits for long calls.

The MCP page returns 404

This is expected when MCP_SECRET is empty. Generate a dedicated secret of at least 32 bytes, different from COOKIE_SECRET, and restart the bot.

Someone received access denied

Confirm they are still in the server. In every channel, they must have joined the call, started the recording, or be a current admin. Later channel access does not unlock history.

The bot refused or stopped because of disk space

Free disk space on the host or carefully adjust MIN_FREE_MB_START and MIN_FREE_MB_ABORT. The abort threshold cannot exceed the start threshold.

Audio is gone, but minutes remain

This is tiered retention. RETENTION_DAYS controls audio and TEXT_RETENTION_DAYS controls transcript, minutes, and notes. Use zero to disable automatic expiration.