Jua
Nunito
Lexend
formerly Verbal Warning Logger

Vigila Your server's memory, enforced.

A Discord moderation bot built for communities that care about accountability. Log verbal warnings, host staff evaluation polls, and migrate from legacy tools.

Want to self-host or learn more? The documentation has everything you need.

What is Vigila?

Vigila (Latin: to watch, to keep guard) is a Discord moderation bot designed for server staff who need reliable, queryable verbal warning records. It was previously known as Verbal Warning Logger — renamed to reflect its broader scope with the introduction of the staff evaluation poll feature and to establish a more distinct identity.

📋

Verbal Warning Logging

Record verbal warnings with reasons, timestamps, and moderator attribution.

🔍

Staff Evaluation Polls

Conduct and manage staff evaluation polls to gather feedback and improve moderation practices.

🗄️

SQLite Database

All data is stored in your own SQLite database locally. You own your data, always.

🔓

Open Source & Self-Hostable

Deploy Vigila on your own infrastructure. Full documentation available.

Why Auttaja + Supabase?

Auttaja was a popular Discord moderation bot that shut down its public service, leaving many communities with years of punishment history trapped in a punishments.json export file with no easy way to use it going forward.

Vigila was built with support for these communities. Rather than starting from scratch, you can import your entire Auttaja punishment history into your own Supabase database in seconds, preserving every warning, mute, kick, and ban record your staff ever issued.

The Supabase integration was chosen specifically because it gives you full control over your data. Your punishments table is yours, readable by standard PostgreSQL tooling, exportable at any time, and secured with Supabase's row-level security (RLS).

The auttaja.py cog bundled with Vigila is mostly read-only. It queries your migrated Auttaja data without modifying it, so your historical records remain exactly as Auttaja left them. You have the option to edit the reasons for past punishments through the /auttaja edit command if you want to clean up any old records.

Migrating from Auttaja

Follow these steps to import your Auttaja punishments.json into Supabase.

01

Create the Supabase Table

Open your Supabase project, go to the SQL Editor, and run the following:

create table punishments (
  id           text primary key,
  guild_id     text not null,
  offender     text not null,
  punisher     text,
  reason       text,
  action       text,           -- normalised to lowercase
  timestamp    timestamptz,
  duration     integer,        -- converted from string to int (seconds)
  deleted      boolean default false,
  removed_by   text,
  removed_reason text,
  resolve      text            -- references another punishment id
);
alter table punishments enable row level security;
02

Install the Supabase JS Client

In your terminal, navigate to the folder containing your punishments.json and run:

npm install @supabase/supabase-js
03

Create import-punishments.mjs

Save the following script in the same directory as your punishments.json. Replace YOUR_PROJECT and YOUR_SERVICE_ROLE_KEY with the values from your Supabase project's Settings → API page. Use the service role key (not the anon key) so the insert bypasses RLS.

// import-punishments.mjs
import { createClient } from '@supabase/supabase-js'
import { readFileSync } from 'fs'

const supabase = createClient(
  'https://YOUR_PROJECT.supabase.co',
  'YOUR_SERVICE_ROLE_KEY'  // use service role key, not anon key
)

const raw = JSON.parse(readFileSync('./punishments.json', 'utf8'))

const records = raw.map(r => ({
  id:             r.id,
  guild_id:       r.guild_id,
  offender:       r.offender,
  punisher:       r.punisher ?? null,
  reason:         r.reason ?? null,
  action:         r.action?.toLowerCase() ?? null,   // normalise Mute/Strike → mute/strike
  timestamp:      r.timestamp?.epoch_time
                    ? new Date(r.timestamp.epoch_time * 1000).toISOString()
                    : null,
  duration:       r.duration != null ? parseInt(r.duration, 10) : null,
  deleted:        r.deleted ?? false,
  removed_by:     r.removed_by ?? null,
  removed_reason: r.removed_reason ?? null,
  resolve:        r.resolve ?? null,
}))

const CHUNK_SIZE = 500

for (let i = 0; i < records.length; i += CHUNK_SIZE) {
  const chunk = records.slice(i, i + CHUNK_SIZE)
  const { error } = await supabase.from('punishments').insert(chunk)
  if (error) {
    console.error(`❌ Chunk ${Math.floor(i / CHUNK_SIZE) + 1} failed:`, error.message)
  } else {
    console.log(`✅ Chunk ${Math.floor(i / CHUNK_SIZE) + 1} inserted (${chunk.length} rows)`)
  }
}

console.log(`\nDone! ${records.length} records processed.`)
04

Run the Script

Execute the import with Node.js:

node import-punishments.mjs

The script processes records in chunks of 500 to avoid payload limits. Each chunk logs a ✅ on success or ❌ with an error message on failure.

Ready to get started?

Full setup guide, command reference, and self-hosting instructions are in the docs.

Go to the Docs