Scope-aware renaming
Local and upvalue names are rewritten with scope awareness, so meaningful names do not survive into the output. Globals, table fields, method names, string keys and type names are never renamed, so behavior is preserved.
vodka / The protection engine
A plain account of how vodka protects your Lua and Luau code: the near-native default pipeline, the opt-in Maximum protection mode, the per-key leak-tracing fingerprint, and the honest limits of anything that runs on a client.
Your script is rewritten into something hard to read and copy, and it still runs. The default pipeline is a pure source rewrite that stays near native, so most scripts need nothing else. When the logic is worth hiding, an opt-in Maximum protection mode adds a per-function bytecode VM. None of it makes a client a vault, and this page says so plainly. See the proof on the benchmarks, and what changed in each release on the changelog.
Applied by default
Every build runs the same set of source transformations. They change how the program is written without changing what it does, so the protected script runs within noise of the original. There is no bytecode VM in the default path.
Local and upvalue names are rewritten with scope awareness, so meaningful names do not survive into the output. Globals, table fields, method names, string keys and type names are never renamed, so behavior is preserved.
String constants are byte-encoded, and their decode key is derived at runtime rather than shipped in plaintext beside the ciphertext. Reading the file does not recover the strings; running the loader does.
Integer literals are rewritten as reversible arithmetic and floats are exact-rewritten, so literal values are not sitting in plain text. A constant-folder reverses the arithmetic, which the benchmarks state plainly.
Supported functions are rearranged into dispatch state-machines, so the logic is harder to read top to bottom, and decoy functions are injected alongside the real ones.
The output is compacted and carries a per-build watermark. The watermark is a build identifier, not a claim of tamper resistance.
These are source rewrites, so default protection lands within measurement noise of the unprotected script. See the measured overhead on the benchmarks.
Opt-in, per script
Turn Maximum protection on per script and supported functions are compiled to bytecode and interpreted by a generated per-function virtual machine, on top of the default transformations. Its operator handlers are table-indirected rather than readable one-line cases, so the interpreter does not read as a plain list of operations.
The VM is selective. It virtualizes only cold functions and keeps hot or looping code (a function with a loop, or one called from inside a loop) native automatically, so Maximum stays near-native on typical code instead of paying the per-function VM cost everywhere. Functions outside the supported set also keep native execution, so behavior is preserved rather than guessed. Per-function directives let you override the choice: --@protect forces a function into the VM, and --@novm or --!native keep one native.
The virtualized bytecode can carry a self-integrity check, so editing its own code breaks the chunk instead of running tampered.
Because the VM is selective, Maximum stays near-native on typical code (around 1.17x on the published fixture, where the hot function stays native). The heavy per-function VM cost only lands on a function you force-virtualize onto a hot path with --@protect; on the published fixture that one forced hot function is roughly 113x. Measure your hot path before forcing the VM onto it.
Per-key leak tracing
Each keyed delivery carries a traceable fingerprint, so a casually leaked copy maps back to the key that received it. The mark is designed to survive minification and a StyLua beautify, so re-indenting the file does not remove it. No-key and premium deliveries are unstamped.
This traces lazy re-sharing, not a determined leaker. An attacker who re-obfuscates or rebuilds the chunk strips the fingerprint, and two buyers who diff their copies can find and remove it. We would rather state that than imply the mark is unbeatable.
Where protection ends
No client-side engine can make a delivered secret unrecoverable. An authorized client receives the code it runs, so running the loader derives the decode key and dumps the entire string pool in plaintext, and a minimal constant-folder reverses the integer arithmetic to nothing. This is true of every obfuscator, vodka included.
What the engine does is raise the cost of casual inspection and copying. What actually gates access is server-side: the key check, expiry, revocation and HWID locks are enforced where the client never sees how the decision was made. Keep anything that must stay secret on a server, and read the benchmarks for exactly what the engine does and does not defeat.
Keep exploring
What shipped in each release, dated and stated plainly, from the initial engine to the current hardened build.
Run the exact harness yourself: protection resistance, runtime overhead and the weaknesses we do not hide.
The same pipeline from the documentation side, with the validated-release flow and the storage model.
The engine as a product page: differential-tested against the real Luau runtime, type annotations preserved.
Questions
Barely. The default pipeline is a pure source rewrite (renaming, encoded strings and numbers, control-flow flattening, a watermark), so it runs within noise of native Luau, roughly 1.14x on the published fixture. The per-function bytecode VM is not in the default path. Measure your own hot path with the Roblox MicroProfiler or an os.clock() A/B loop before you ship.
It adds the per-function bytecode VM with table-indirected operator handlers and an opt-in self-integrity anti-tamper check, on top of the default transformations. The VM is selective: it virtualizes only cold functions and keeps hot or looping code native automatically, so Maximum stays near-native on typical code (around 1.17x on the published fixture, where the hot function stays native). The heavy per-function VM cost only lands if you force the VM onto a hot path with the --@protect directive; --@novm and --!native keep a function native. Turn Maximum on per script when the logic is worth hiding; the engine keeps your frame-time hot path native unless you force it otherwise.
No, and we will not claim otherwise. Running the loader derives the decode key and recovers the whole string pool in plaintext, and a constant-folder reverses the integer arithmetic. Any value shipped to an authorized client is recoverable. The engine raises the cost of casual inspection and copying; it does not make a client a vault. Keep secrets and access decisions on the server.
It traces casual re-sharing. Each keyed delivery carries a traceable fingerprint that maps a leaked copy back to the key that received it, and the mark now survives minification and a StyLua beautify. A determined attacker who re-obfuscates or rebuilds the chunk strips it, and two buyers who diff their copies can find and remove it. No-key and premium deliveries are unstamped.
Keep building
Default protection is near native and on by every build. Bring your source and see for yourself.