Skip to content

Starter Template & Pro Showcase

Everything you need to go from zero to published addon. A production-ready template, real-world examples from top addons, modern tooling, and an AI-friendly workflow that lets you build faster than ever.


1. The Perfect Starter Template

No existing addon template targets Midnight 12.0+. Ours does. It ships with the correct Interface version, Secret Values awareness, CI/CD, linting, AI instructions, and every file a modern addon needs.

What's Included (and Why)

File Purpose
MyAddon.toc Addon manifest — Interface 120001, SavedVariables, load order
Core.lua Entry point — namespace setup, event dispatcher, initialization
Config.lua SavedVariables defaults, migration system, settings panel
Events.lua Event handler registrations, combat deferral utilities
UI.lua Frame creation, layout, skinning helpers
Utils.lua Shared utilities, secret value helpers
Locales/enUS.lua Localization strings
Libs/embeds.xml Library loader (LibStub, CallbackHandler, AceDB)
.pkgmeta BigWigsMods/packager config — library externals, packaging rules
.luacheckrc Luacheck config with WoW API globals
.luarc.json VS Code LuaLS config for IntelliSense
.editorconfig Consistent tabs, line endings, charset
.github/workflows/release.yml Tag push auto-publishes to CurseForge + Wago + GitHub
.github/workflows/lint.yml Luacheck runs on every push and PR
CLAUDE.md AI instructions — 12.0 API rules, deprecated functions, patterns
.cursorrules Cursor IDE instructions (mirrors CLAUDE.md)

Full File Tree

MyAddon/
├── .github/
│   └── workflows/
│       ├── release.yml          # Tag → package → upload everywhere
│       └── lint.yml             # Luacheck on push/PR
├── .vscode/
│   ├── extensions.json          # Recommends ketho.wow-api + StyLua
│   └── settings.json            # LuaLS Lua 5.1 config
├── Libs/                        # Populated by .pkgmeta at build time
│   └── embeds.xml               # XML library loader
├── Locales/
│   └── enUS.lua                 # English strings
├── MyAddon.toc                  # Interface: 120001
├── Core.lua                     # Namespace + init + event dispatch
├── Config.lua                   # SavedVariables + defaults + migration
├── Events.lua                   # Event registrations + combat deferral
├── UI.lua                       # Frames + skinning helpers
├── Utils.lua                    # Utilities + secret value helpers
├── CLAUDE.md                    # AI coding instructions
├── .cursorrules                 # Cursor IDE instructions
├── .luacheckrc                  # Lua linter config
├── .luarc.json                  # VS Code language server config
├── .pkgmeta                     # Packager config + library externals
├── .editorconfig                # Editor settings
├── .gitignore                   # Ignores .release/, Libs/, *.zip
├── CHANGELOG.md
├── LICENSE
└── README.md

File Walk-Through

MyAddon.toc

The manifest that tells WoW how to load your addon.

## Interface: 120001
## Title: MyAddon
## Notes: A brief description of what your addon does
## Author: YourName
## Version: @project-version@
## SavedVariables: MyAddonDB
## OptionalDeps: LibStub, CallbackHandler-1.0, Ace3
## IconTexture: Interface\AddOns\MyAddon\icon
## X-Curse-Project-ID: 000000
## X-Wago-ID: aBcDeFgH

#@no-lib-strip@
Libs\embeds.xml
#@end-no-lib-strip@

Locales\enUS.lua

Core.lua
Utils.lua
Config.lua
Events.lua
UI.lua

@project-version@

Replaced with your git tag at build time by BigWigsMods/packager. Tag v1.0.0 → Version shows as v1.0.0.

#@no-lib-strip@

Tells the packager to strip library loading lines from the -nolib zip variant. Users who have standalone library addons installed won't double-load.

Core.lua — The Entry Point

local addonName, ns = ...

-- ─── Version detection ───────────────────────────────────
ns.isRetail = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE
ns.isMidnight = ns.isRetail and (select(4, GetBuildInfo()) >= 120000)

-- ─── Addon version ───────────────────────────────────────
ns.version = C_AddOns.GetAddOnMetadata(addonName, "Version") or "dev"

-- ─── Slash command ───────────────────────────────────────
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function(msg)
    local cmd = strlower(strtrim(msg))
    if cmd == "config" or cmd == "options" then
        Settings.OpenToCategory(addonName)
    else
        print("|cff00ccff" .. addonName .. "|r v" .. ns.version)
        print("  /myaddon config — Open settings")
    end
end

-- ─── Initialization via EventUtil (modern 12.0 pattern) ──
EventUtil.ContinueOnAddOnLoaded(addonName, function()
    ns.InitDB()          -- Config.lua
    ns.InitEvents()      -- Events.lua
    ns.InitUI()          -- UI.lua
    print("|cff00ccff" .. addonName .. "|r v" .. ns.version .. " loaded.")
end)

Utils.lua — Secret Value Helpers

local addonName, ns = ...

-- ─── Secret Values (Midnight 12.0) ──────────────────────
ns.SECRETS_ENABLED = type(issecretvalue) == "function"

function ns.SafeValue(val, fallback)
    if ns.SECRETS_ENABLED and issecretvalue(val) then
        return fallback
    end
    return val
end

-- ─── Combat deferral ─────────────────────────────────────
ns.combatQueue = {}

function ns.AfterCombat(fn)
    if InCombatLockdown() then
        table.insert(ns.combatQueue, fn)
    else
        fn()
    end
end

function ns.FlushCombatQueue()
    for _, fn in ipairs(ns.combatQueue) do
        fn()
    end
    wipe(ns.combatQueue)
end

-- ─── Debounce ────────────────────────────────────────────
function ns.Debounce(delay, fn)
    local timer
    return function(...)
        if timer then timer:Cancel() end
        local args = {...}
        timer = C_Timer.NewTimer(delay, function()
            fn(unpack(args))
        end)
    end
end

Config.lua — SavedVariables with Migration

local addonName, ns = ...

ns.DEFAULTS = {
    enabled = true,
    scale = 1.0,
    position = { point = "CENTER", x = 0, y = 0 },
    theme = "dark",
    _version = 1,
}

function ns.InitDB()
    -- Merge saved data with defaults
    MyAddonDB = MyAddonDB or {}
    for key, default in pairs(ns.DEFAULTS) do
        if MyAddonDB[key] == nil then
            if type(default) == "table" then
                MyAddonDB[key] = CopyTable(default)
            else
                MyAddonDB[key] = default
            end
        end
    end
    ns.db = MyAddonDB
    ns.MigrateDB()
end

function ns.MigrateDB()
    local db = ns.db
    -- v2: example migration
    if (db._version or 1) < 2 then
        -- Rename old keys, restructure data, etc.
        db._version = 2
    end
end

-- ─── Settings panel (Blizzard Settings API) ──────────────
function ns.RegisterSettings()
    local category = Settings.RegisterCanvasLayoutCategory(
        ns.CreateSettingsPanel(), addonName
    )
    Settings.RegisterAddOnCategory(category)
end

Events.lua — Event Handling + Combat Safety

local addonName, ns = ...

function ns.InitEvents()
    -- Flush combat queue when combat ends
    EventRegistry:RegisterFrameEventAndCallback(
        "PLAYER_REGEN_ENABLED", ns.FlushCombatQueue
    )

    -- Example: react to bag updates with debounce
    local debouncedRefresh = ns.Debounce(0.05, function()
        if ns.RefreshUI then ns.RefreshUI() end
    end)
    EventRegistry:RegisterFrameEventAndCallback(
        "BAG_UPDATE_DELAYED", debouncedRefresh
    )
end

.pkgmeta — Library Externals

package-as: MyAddon
enable-nolib-creation: yes

externals:
  Libs/LibStub:
    url: https://repos.curseforge.com/wow/libstub/trunk
    tag: latest
  Libs/CallbackHandler-1.0:
    url: https://repos.curseforge.com/wow/callbackhandler/trunk/CallbackHandler-1.0
    tag: latest

ignore:
  - .github
  - .vscode
  - .luacheckrc
  - .luarc.json
  - .editorconfig
  - .cursorrules
  - CLAUDE.md
  - README.md
  - CHANGELOG.md

.github/workflows/release.yml

name: Package and release

on:
  push:
    tags:
      - "**"

jobs:
  release:
    runs-on: ubuntu-latest
    env:
      CF_API_KEY: ${{ secrets.CF_API_KEY }}
      WOWI_API_TOKEN: ${{ secrets.WOWI_API_TOKEN }}
      WAGO_API_TOKEN: ${{ secrets.WAGO_API_TOKEN }}
      GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for changelog generation
      - uses: BigWigsMods/packager@v2

.github/workflows/lint.yml

name: Lint

on:
  push:
    branches: [main]
  pull_request:

jobs:
  luacheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: nebularg/actions-luacheck@v1
        with:
          args: "--no-color -q"
          annotate: warning

.luacheckrc

std = "lua51"
max_line_length = false
exclude_files = { "Libs/", ".release/" }

ignore = {
    "11./SLASH_",     -- Setting slash command globals
    "212/self",       -- Unused self in mixin methods
    "212/event",      -- Unused event arg
}

globals = {
    "MyAddonDB",
    "SLASH_MYADDON1",
    "SlashCmdList",
}

read_globals = {
    "LibStub",
    "CreateFrame", "UIParent", "GameTooltip",
    "C_Timer", "C_AddOns", "C_EditMode",
    "Mixin", "CreateFromMixins", "BackdropTemplateMixin",
    "EventUtil", "EventRegistry", "Settings",
    "InCombatLockdown", "GetBuildInfo", "GetLocale",
    "hooksecurefunc", "issecretvalue",
    "WOW_PROJECT_ID", "WOW_PROJECT_MAINLINE",
    "strlower", "strtrim", "wipe", "CopyTable",
    "print", "format", "select", "type", "unpack",
}

Auto-generated luacheckrc

Jayrgo/wow-luacheckrc auto-generates a complete .luacheckrc from Blizzard's own interface resources, covering every known WoW global. Far more comprehensive than hand-maintained configs.


2. Showcase: How The Pros Do It

BetterBags — Modern Clean Architecture

Cidan/BetterBags | MIT License | Midnight Ready

BetterBags/
├── .github/workflows/          # release + alpha + luacheck + busted tests + claude
├── CLAUDE.md, AGENTS.md        # AI context files
├── BetterBags.toc              # Interface: 120000, 120001
├── BetterBags_Vanilla.toc, _TBC.toc, _Mists.toc
├── annotations.lua             # LuaCATS type annotations throughout
├── core/
│   ├── boot.lua                # FIRST loaded — AceAddon:NewAddon + SetDefaultModuleState(false)
│   ├── init.lua                # LAST loaded — OnInitialize/OnEnable
│   ├── async.lua               # Coroutine batch rendering (frame-rate aware)
│   ├── context.lua             # Go-style context system
│   ├── database.lua            # AceDB wrapper with migration
│   ├── events.lua              # Custom event layer (bucketing, deferred send)
│   └── pool.lua                # Object pooling
├── data/                       # Data layer (items, categories, refresh)
├── frames/                     # UI components (~25 files)
├── views/                      # View layer (bag, grid)
├── themes/                     # Pluggable themes (default, simpledark, gw2, elvui)
├── integrations/               # Masque, ConsolePort, Pawn
├── config/                     # Custom form framework (NOT AceConfig)
├── spec/                       # Unit tests with busted
└── examples/plugin/            # Plugin example for third-party devs

What makes it great:

  • Modules start disabledSetDefaultModuleState(false), enabled selectively in OnEnable
  • Coroutine batch rendering — adapts batch size to framerate, yields between batches
  • Go-style contexts — passed as first arg to every callback for cancellation/timeout
  • AI-ready — ships CLAUDE.md and AGENTS.md, has a claude.yml workflow
  • Theme registry — pluggable themes let users completely restyle the UI
  • Unit tests — busted test framework runs in CI

Key patterns to steal: Boot/Init split, debounced refresh with combat deferral, hidden parent taint isolation, object pooling.


Cell — Gold Standard Raid Frames

enderneko/Cell | 186 stars | Interface 120001

Cell/
├── Cell.toc + 5 flavor TOCs
├── Core.lua                    # Single global: _G.Cell = select(2, ...)
├── Utils.lua, Revise.lua       # Utilities + DB migration
├── HideBlizzard.lua            # Hides native raid frames
├── Libs/
│   ├── CallbackHandler.lua     # Cell's OWN lightweight callback (not Ace3)
│   ├── PixelPerfect.lua        # Own pixel-perfect helper
│   └── [External libs via .pkgmeta]
├── Defaults/                   # Settings defaults per category, per flavor
├── Indicators/                 # Visual indicators (9 files)
├── RaidFrames/                 # Core: MainFrame, UnitButton, Groups/
├── Modules/                    # Options UI
├── RaidDebuffs/                # Per-expansion debuff data
├── Utilities/                  # BattleRes, BuffTracker, DeathReport, Marks
├── Widgets/                    # Animation, ColorPicker, Tooltip
├── Locales/                    # 11 languages
└── .snippets/                  # 30+ user code snippet examples

What makes it great:

  • Zero framework dependency — custom namespace, custom callbacks, custom SavedVariables. No Ace3.
  • Self-dispatching eventseventFrame[event](self, ...) pattern for zero-allocation dispatch
  • Custom callback systemCell.Fire("UpdateLayout") pub/sub without library overhead
  • Per-expansion data — RaidDebuffs split by expansion for easy maintenance
  • 6 flavor TOCs with flavor-specific Lua files

Key patterns to steal: Custom lightweight callback system, self-dispatching event frame, SavedVariables validation without AceDB.


OmniCC — Minimal & Modern

tullamods/OmniCC | Secret Values handling

OmniCC/
├── OmniCC/                     # Main addon (monorepo)
│   ├── OmniCC.toc              # Interface: 110207, 50503, 38000, 20505, 11508
│   ├── main.lua                # Entry point — EventUtil, no ADDON_LOADED frame!
│   ├── core/
│   │   ├── cooldown.lua        # Hooks Cooldown widget METATABLE globally
│   │   ├── timer.lua           # Timer pool with subscriber pattern
│   │   ├── display.lua         # FontString displays
│   │   └── types.lua           # LuaCATS annotations
│   ├── effects/                # Finish effects (alert, flare, pulse, shine)
│   ├── settings/               # Themes + pattern-matching rules
│   └── localization/
├── OmniCC_Config/              # Separate LoadOnDemand config addon
│   ├── OmniCC_Config.toc       # LoadOnDemand: 1
│   └── options.lua, preview.lua, rules.lua, themes.lua
└── .pkgmeta                    # move-folders separates addons at build time

What makes it great:

  • Modern EventUtil initEventUtil.ContinueOnAddOnLoaded() instead of the traditional ADDON_LOADED frame
  • Metatable-level hooks — hooks ALL cooldown widgets by hooking the Cooldown widget metatable
  • Secret Values handling — detects SECRETS_ENABLED, uses proxy frames to reverse-engineer durations
  • LoadOnDemand config — settings UI only loads when opened, reducing memory
  • Monorepo with move-folders — two addons in one repo, separated at build time

Key patterns to steal: EventUtil initialization, metatable hooking, LoadOnDemand config addon, Secret Values detection.


EditModeExpanded — Edit Mode Library

teelolws/EditModeExpanded | Interface 120001

EditModeExpanded/
├── Source/                     # Moved to root at build time
│   ├── EditModeExpanded.toc
│   ├── EditModeExpanded.lua    # Main init — EventUtil throughout
│   ├── FrameHandlers/          # One file per frame (35 handlers!)
│   │   ├── ActionBars.lua, Buffs.lua, ChatFrame.lua, Housing.lua
│   │   ├── Minimap.lua, ObjectiveTracker.lua, Pet.lua, Tooltip.lua, ...
│   ├── RegisterFrame.lua       # Core frame registration API
│   ├── CombatManager.lua       # continueAfterCombatEnds() utility
│   ├── HookOnce.lua            # One-time hook utilities
│   └── libs/
│       └── EditModeExpanded-1.0/  # Reusable library for other addons

What makes it great:

  • One handler per frame — 35 clean files in FrameHandlers/, easy to find and modify
  • Reusable libraryEditModeExpanded-1.0 via LibStub, other addons can register frames
  • EventUtil everywhere — zero manual frame event registration
  • Lazy Blizzard addon loadingEventUtil.ContinueOnAddOnLoaded("Blizzard_AuctionHouseUI", ...)
  • hookScriptOnce / hookFuncOnce — clean one-time hooks

Key patterns to steal: File-per-frame organization, reusable library pattern, one-time hook utilities.


AdiBags — Modular Bag Addon

AdiAddons/AdiBags | Predecessor to BetterBags

AdiBags/
├── AdiBags.toc + 4 flavor TOCs
├── core/
│   ├── EventHandlers.lua       # Loaded FIRST — creates custom event libraries
│   ├── Boot.lua                # AceAddon:NewAddon — starts DISABLED
│   ├── Core.lua, Hooks.lua, Layout.lua, Filters.lua
│   ├── ItemDatabase.lua, Theme.lua, OO.lua
├── modules/                    # 12 feature modules (Masque, Junk, ItemLevel, ...)
├── widgets/                    # UI: ItemButton, Section, ContainerFrame, grid/
├── config/                     # Options
├── AdiBags_Config/             # Separate LoadOnDemand config addon
└── tests/

What makes it great:

  • Custom event librariesABEvent-1.0 and ABBucket-1.0 via LibStub, reusable by any addon
  • Starts disabledSetEnabledState(false), enables on PLAYER_ENTERING_WORLD, disables on PLAYER_LEAVING_WORLD
  • Plugin system — filter plugins register categorization rules: AdiBags:RegisterFilter("MyFilter", priority)
  • Event bucketsself:RegisterBucketEvent("BAG_UPDATE", 0.01, "BagUpdated") for debouncing

Key patterns to steal: Custom event libraries via LibStub, delayed enablement, plugin/filter system.


Plater — Nameplate Powerhouse

Tercioo/Plater-Nameplates | Interface 120001

Plater/
├── .github/workflows/
│   ├── CurseForge-Deployment.yml
│   └── TOCBump.yml             # Daily cron → auto-updates Interface versions!
├── Plater.lua                  # Main addon (~5000 lines, event dispatch table)
├── Plater_DefaultSettings.lua  # Massive defaults table
├── Plater_API.lua              # Public API
├── Plater_ScriptLibrary.lua    # User script sandbox
├── Plater_LoadFinished.lua     # LAST loaded — sets up load-on-demand subsystems
├── libs/
│   └── DF/                     # Details Framework — massive UI library
├── options/                    # Load-on-demand option panels
├── locales/, fonts/, images/, masks/, media/, sounds/

What makes it great:

  • Auto TOC bump — daily cron workflow auto-updates Interface versions
  • Event dispatch tablehandlers["NAME_PLATE_CREATED"] = function(...) end with per-handler CPU profiling
  • User script sandbox — lets users write custom Lua scripts that run in a controlled environment
  • Dual namespaceplaterInternal (private) + Plater (global via Details Framework)

Key patterns to steal: Auto TOC bump workflow, event dispatch table with profiling, user script system.


3. Template Comparison Matrix

Feature Ours kapresoft layday Kkthnx Ketho HelloWorld
Interface 120001 ✅ ❌ ❌ ❌ ❌
Secret Values awareness ✅ ❌ ❌ ❌ ❌
EventUtil init pattern ✅ ❌ ❌ ❌ ❌
BigWigsMods/packager CI ✅ ✅ ✅ ❌ ❌
Luacheck linting ✅ ❌ ✅ ❌ ❌
.luarc.json (VS Code) ✅ ❌ ❌ ❌ ❌
.pkgmeta externals ✅ ✅ ✅ ❌ ❌
SavedVariables + migration ✅ ❌ ❌ ✅ ✅
Combat deferral utilities ✅ ❌ ❌ ❌ ❌
CLAUDE.md (AI instructions) ✅ ❌ ❌ ❌ ❌
.cursorrules ✅ ❌ ❌ ❌ ❌
Multi-flavor TOC ❌ ✅ ❌ ❌ ❌
Ace3 integration Optional ✅ ❌ ✅ ❌
Last updated 2026 2024 2020 2023 (archived) 2025

Bottom line: Existing templates are outdated. None target Midnight, none handle Secret Values, and none include AI tooling. Our template fills every gap.


4. The AI-Friendly Addon Project

Why Structure Matters for AI

AI tools generate correct code when they have the right context upfront. Every file in this template serves double duty:

For Humans For AI
Documentation Context that prevents hallucination
Code examples Patterns to extrapolate from
Linting rules Guardrails against deprecated APIs
File structure Signals for where to put new code

CLAUDE.md Best Practices

The CLAUDE.md file is your behavioral specification for AI. It's the difference between Claude hallucinating removed APIs and generating working 12.0 code on the first try.

# MyAddon

## Critical: WoW 12.0 Midnight API Rules

### DO NOT USE (Removed/Broken):
- COMBAT_LOG_EVENT_UNFILTERED — removed in 12.0
- CombatLogGetCurrentEventInfo() — removed
- UnitHealth() for math — returns secret values during combat

### Secret Values (CRITICAL):
- During M+/PvP/encounters, combat data returns as opaque tokens
- Always check: if issecretvalue(val) then return end
- Use CurveObject/DurationObject for visual display

### Interface Version:
- Current: 120001 (Patch 12.0.1)
- Addons below 120000 will NOT load

## Architecture

### File Load Order (defined in .toc):
1. Libs/embeds.xml — External libraries
2. Locales/enUS.lua — Strings
3. Core.lua — Namespace + init
4. Utils.lua — Shared utilities
5. Config.lua — SavedVariables + defaults
6. Events.lua — Event handlers
7. UI.lua — Frame creation

### Namespace Pattern:
local addonName, ns = ...
-- All code uses ns.* to share between files
-- NEVER create globals except slash commands

## Coding Conventions

### Always:
- Use `local` for everything
- Check issecretvalue() before comparing combat values
- Check InCombatLockdown() before modifying secure frames
- Use hooksecurefunc() for post-hooks (never wrap secure functions)
- Use BAG_UPDATE_DELAYED (not BAG_UPDATE)

### Never:
- Generate CLEU handlers
- Call functions listed as removed in 12.0
- Create globals without explicit declaration in .luacheckrc
- Call SetScript() on Blizzard frames (use HookScript)

Claude Agents for Addon Dev

Place these in .claude/agents/ for specialized AI assistance:

Agent File Role
wow-addon-coder.md Writes Lua code following verified 12.0 patterns. Knows Secret Values. Uses local everywhere.
wow-addon-researcher.md Searches warcraft.wiki.gg for API docs. Verifies function signatures exist in 12.0.

The AI Workflow

You: "Add a minimap button that toggles the config panel"

Claude reads CLAUDE.md → knows the namespace pattern, load order, 12.0 APIs
Claude reads Config.lua → sees existing settings panel registration
Claude adds LibDBIcon to .pkgmeta externals
Claude updates embeds.xml to load LibDBIcon
Claude adds minimap button code to UI.lua using ns.db for toggle state
Claude updates .luacheckrc with new globals
You: /reload → it works

The key insight: AI tools extrapolate from your existing patterns. One well-structured file teaches it how to write the next ten. A messy codebase produces messy AI output.


5. Modern Toolchain Setup

IDE Setup (VS Code)

Install three extensions:

ext install sumneko.lua          # LuaLS language server
ext install ketho.wow-api        # WoW API autocomplete (8,000+ functions)
ext install stanzilla.vscode-wow-toc  # TOC file syntax highlighting

ketho.wow-api auto-activates when it detects a .toc file. Covers all 12.0.1 APIs with signatures, parameter types, and wiki links.

.vscode/settings.json:

{
    "Lua.runtime.version": "Lua 5.1",
    "Lua.runtime.builtin": {
        "io": "disable",
        "os": "disable",
        "package": "disable",
        "debug": "disable"
    },
    "Lua.workspace.ignoreDir": ["Libs", ".release", ".github"],
    "Lua.diagnostics.disable": ["lowercase-global"],
    "Lua.workspace.checkThirdParty": true
}

.vscode/extensions.json:

{
    "recommendations": [
        "sumneko.lua",
        "ketho.wow-api",
        "stanzilla.vscode-wow-toc",
        "johnnymorganz.stylua"
    ]
}

Linting with Luacheck

# Install
brew install luacheck    # macOS
luarocks install luacheck # via luarocks

# Run
luacheck .

# In CI: nebularg/actions-luacheck@v1 (see lint.yml above)

Auto-generated configs

Jayrgo/wow-luacheckrc parses Blizzard's own interface resources to generate a .luacheckrc covering every known WoW global. Alternatively, nebularg/wow-selene-parser generates configs for the Selene linter.

Packaging with BigWigsMods/packager

The industry standard. Used by every major addon — BetterBags, Cell, Plater, OmniCC, WeakAuras, ElvUI, Bartender4, BigWigs, DBM.

How it works:

  1. You push a git tag: git tag v1.0.0 && git push --tags
  2. GitHub Actions triggers BigWigsMods/packager@v2
  3. Packager checks out library externals from .pkgmeta
  4. Replaces @project-version@ tokens in your code
  5. Strips @debug@ blocks from release builds
  6. Creates zip files (including -nolib variant)
  7. Uploads to CurseForge, WoWInterface, Wago, and GitHub Releases

Keyword substitutions (replaced at build time):

Keyword Becomes
@project-version@ Your git tag (e.g., v1.0.0)
@project-hash@ Full git commit hash
@project-date-iso@ ISO date of last commit
@debug@ ... @end-debug@ Stripped from release builds
@alpha@ ... @end-alpha@ Active only in untagged builds

CI/CD with GitHub Actions

Secrets to configure (Settings > Secrets and variables > Actions):

Secret Platform Where to Generate
CF_API_KEY CurseForge Author dashboard > API tokens
WOWI_API_TOKEN WoWInterface File management > API tokens
WAGO_API_TOKEN Wago Addons addons.wago.io/account/apikeys
GITHUB_TOKEN GitHub Releases Auto-provided (enable write permissions)

Enable workflow write permissions

Settings > Actions > General > Workflow permissions > Read and write permissions. Required for GitHub Releases.

Publishing to CurseForge / Wago

  1. Create the project on CurseForge and/or Wago
  2. Get your project IDs and add them to your TOC:
    ## X-Curse-Project-ID: 123456
    ## X-Wago-ID: aBcDeFgH
    
  3. Add API key secrets to your GitHub repo
  4. Push a tag — the workflow handles everything:
    git tag v1.0.0
    git push --tags
    

6. Quick Start Guide

Step 1: Fork & Clone

# Clone the template
git clone https://github.com/YourOrg/wow-addon-template.git MyAddon
cd MyAddon

# Remove template git history, start fresh
rm -rf .git
git init
git add -A
git commit -m "Initial commit from Midnight addon template"

Step 2: Rename Everything

Find and replace these strings across all files:

Find Replace With
MyAddon Your addon name (e.g., CoolBags)
MYADDON Uppercase version (e.g., COOLBAGS)
MyAddonDB Your SavedVariables name (e.g., CoolBagsDB)
YourName Your author name

Files to rename:

  • MyAddon.tocCoolBags.toc

Step 3: Customize

  1. Edit the TOC — update Title, Notes, Author
  2. Edit Core.lua — change the slash command, add your init logic
  3. Edit Config.lua — define your default settings
  4. Edit UI.lua — build your frames
  5. Update .luacheckrc — add your global names

Step 4: Set Up Dev Environment

# macOS: symlink into WoW's AddOns folder
ln -s "$(pwd)" \
  "/Applications/World of Warcraft/_retail_/Interface/AddOns/CoolBags"
# Windows (admin PowerShell):
New-Item -ItemType SymbolicLink `
  -Path "D:\Games\World of Warcraft\_retail_\Interface\AddOns\CoolBags" `
  -Value "D:\repos\CoolBags"

Step 5: Test In-Game

  1. Launch WoW
  2. Type /reload to reload the UI
  3. Your addon loads — check for errors with /console scriptErrors 1
  4. Install BugSack + BugGrabber for proper error capture with stack traces

Step 6: Iterate

Edit code → Save → Alt-Tab to WoW → /reload → Test → Repeat

No hot-reload exists. /reload restarts the full addon lifecycle.

Step 7: Release

# Create GitHub repo
gh repo create CoolBags --public --source=. --push

# Configure secrets (CF_API_KEY, WAGO_API_TOKEN, etc.)
# Then tag and push:
git tag v1.0.0
git push --tags

The CI pipeline packages your addon and uploads it to CurseForge, Wago, and GitHub Releases automatically.

You did it

Your addon is live. Users can install it from CurseForge or Wago. Push a new tag whenever you want to release an update.


Essential Resources

Resource URL
Warcraft Wiki API warcraft.wiki.gg/wiki/World_of_Warcraft_API
12.0 API Changes warcraft.wiki.gg/wiki/Patch_12.0.0/API_changes
Blizzard FrameXML Source github.com/Gethe/wow-ui-source
Ketho's API Resources github.com/Ketho/BlizzardInterfaceResources
VS Code WoW API Extension ketho.wow-api
BigWigsMods Packager github.com/BigWigsMods/packager
Packager Wiki github.com/BigWigsMods/packager/wiki
WoW Addon Dev Guide (AI) github.com/Amadeus-/WoWAddonDevGuide
Copilot Agents for WoW github.com/JBurlison/WoWAddonAPIAgents
TOC Format Spec warcraft.wiki.gg/wiki/TOC_format