Start typing to search packages!
vanguard
By @twrblxdevs
Roblox
MirroredVanguard
Vanguard is a dependency-free Roblox application framework packaged for Wally. It keeps the best parts of Knit: server services, client controllers, lifecycle hooks, and declarative remotes, then adds framework-grade pieces like network validation and authentication, tagged components, registered classes, caching, rate limiting, scoped logging, centralized config, and cleaner bootstrapping.
Documentation
The complete documentation lives at twrblxdevs.github.io/vanguard-docs.
- Getting Started
- Lifecycle
- Services, Controllers, Components, and Classes
- Networking and Network Security
- Errors and the complete Utilities index
- API Reference, Type System, and Troubleshooting
- Roadmap for upcoming platform, tooling, and community work
- Contributing for setup, commit conventions, testing, and pull request expectations
Install
Add Vanguard to a game's wally.toml:
[dependencies]
Vanguard = "twrblxdevs/vanguard@0.1.15"
Install packages:
wally install
Map Wally packages into your Rojo project:
{
"name": "MyGame",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"Packages": {
"$path": "Packages"
}
}
}
}
Require Vanguard:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Vanguard = require(ReplicatedStorage.Packages.Vanguard)
For best Studio autocomplete, prefer the direct package path above. WaitForChild returns a generic Instance, which can make Luau show *error-type* unless you add extra type casts.
If you need WaitForChild, keep the static type from the direct path:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Packages = ReplicatedStorage:WaitForChild("Packages")
local Vanguard = require(Packages:WaitForChild("Vanguard") :: ModuleScript) :: typeof(require(ReplicatedStorage.Packages.Vanguard))
Types
Vanguard exports Luau types from the main module:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Vanguard = require(ReplicatedStorage.Packages.Vanguard)
type Vanguard = Vanguard.Vanguard
type StartOptions = Vanguard.StartOptions
type Service = Vanguard.Service
type Controller = Vanguard.Controller
type Component = Vanguard.Component
type Class = Vanguard.Class
type PluginDefinition = Vanguard.PluginDefinition
type Cache = Vanguard.Cache
type Replicator = Vanguard.Replicator
type VanguardError = Vanguard.VanguardError
type MathUtil = Vanguard.MathUtil
type RateLimiter = Vanguard.RateLimiter
type NetworkRule = Vanguard.NetworkRule
type NetworkContext = Vanguard.NetworkContext
type NetworkRejection = Vanguard.NetworkRejection
type Logger = Vanguard.Logger
type Promise = Vanguard.Promise
type RemoteSignal = Vanguard.RemoteSignal
type RemoteProperty = Vanguard.RemoteProperty
type Switch = Vanguard.Switch
For service/controller-specific shapes, intersect your own fields with Vanguard's base types:
type InventoryService = Vanguard.Service & {
Client: {
InventoryChanged: Vanguard.RemoteSignal,
GetItems: (self: any, player: Player) -> { string },
},
GetItems: (self: any, player: Player) -> { string },
}
Utility modules export types too:
local Promise = require(Vanguard.Util.Promise)
local Cleaner = require(Vanguard.Util.Cleaner)
type Promise = Promise.Promise
type Cleaner = Cleaner.Cleaner
Layout
This repository is package-only now:
src
shared
Vanguard
init.luau
VanguardServer.luau
VanguardClient.luau
Util
default.project.json intentionally points directly at src/shared/Vanguard so Wally publishes a single module package.
Bootstrapping
Server:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Vanguard = require(ReplicatedStorage.Packages.Vanguard)
Vanguard.Bootstrap({
Services = script.Parent.Services,
Components = script.Parent.Components,
Classes = script.Parent.Classes,
Plugins = script.Parent.Plugins,
Options = {
LogLevel = "info",
},
}):catch(warn)
Client:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Vanguard = require(ReplicatedStorage.Packages.Vanguard)
Vanguard.Bootstrap({
Controllers = script.Parent.Controllers,
Components = script.Parent.Components,
Classes = script.Parent.Classes,
Plugins = script.Parent.Plugins,
Options = {
LogLevel = "info",
},
}):catch(warn)
You can also load and start manually with AddServicesDeep, AddControllersDeep, AddComponentsDeep, and Start. Public Vanguard methods support both dot and colon syntax, so Vanguard.GetService("InventoryService") and Vanguard:GetService("InventoryService") both work.
Services and controllers receive a scoped Logger automatically. Vanguard does not inject the full framework table into services, controllers, or client service proxies, so GetService and GetServices stay focused on your registered services.
Startup Logs
Vanguard logs startup by default at info level:
[Vanguard] [INFO] Starting server v0.1.15 (3 services, 2 components)
[Vanguard] [INFO] Server startup complete in 12ms
[Vanguard] [INFO] Starting client v0.1.15 (4 controllers, 1 component)
[Vanguard] [INFO] Client startup complete in 8ms
Use debug for individual services/controllers/components:
Vanguard.Start({
LogLevel = "debug",
})
Use warn or silent to reduce framework output.
Update Checks
On the server, Vanguard checks the Wally index after startup and warns if a newer published version exists:
[VANGUARD OUTDATED]
============================================================
Your installed Vanguard version is out of date.
Installed: 0.1.14
Latest: 0.1.15
...
============================================================
The check is non-blocking and only logs at debug level if HTTP is unavailable, disabled in Studio, or the registry cannot be reached. Wally index files contain one JSON entry per published version, and Vanguard picks the highest version from that list. Make sure HttpService is enabled in the game if you want version warnings.
Disable the check:
Vanguard.Start({
CheckForUpdates = false,
})
Use a custom package index URL:
Vanguard.Start({
UpdateCheckUrl = "https://raw.githubusercontent.com/UpliftGames/wally-index/main/twrblxdevs/vanguard",
})
Services
Services run on the server. Anything in the Client table becomes available to clients. Service modules can either call Vanguard.CreateService(...) or return a plain service table; AddServices and AddServicesDeep register returned tables automatically.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Vanguard = require(ReplicatedStorage.Packages.Vanguard)
local InventoryService = Vanguard.CreateService({
Name = "InventoryService",
Priority = 0,
Client = {
InventoryChanged = Vanguard.CreateSignal(),
},
})
function InventoryService.Client:GetItems(player)
return self.Server:GetItems(player)
end
function InventoryService:GetItems(player)
return {}
end
function InventoryService:VanguardStart()
self.Logger:Info("Ready")
end
return InventoryService
Lifecycle hooks:
function Service:VanguardInit()
-- Runs after all services are registered.
end
function Service:VanguardStart()
-- Runs after every VanguardInit finishes.
end
KnitInit and KnitStart are supported for migration.
Services can set Priority to control startup order. Higher numbers initialize first and have their start hooks scheduled first; services with the same priority keep the default alphabetical ordering. VanguardInit waits for each priority batch before moving to the next lower priority.
self.Server inside Client methods is provided only while that remote method is running, so your printed service table will not contain a circular Client.Server reference.
Controllers
Controllers run on the client. Controller modules can either call Vanguard.CreateController(...) or return a plain controller table; AddControllers and AddControllersDeep register returned tables automatically.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Vanguard = require(ReplicatedStorage.Packages.Vanguard)
local InventoryController = Vanguard.CreateController({
Name = "InventoryController",
})
function InventoryController:VanguardStart()
local InventoryService = Vanguard.GetService("InventoryService")
InventoryService:GetItems():andThen(function(items)
self.Items = items
end):catch(function(err)
self.Logger:Warn(err)
end)
end
return InventoryController
Client service methods return promises by default. Use yielding calls instead with:
Vanguard.Start({
ServicePromises = false,
})
Components
Components attach behavior to instances tagged with CollectionService. They work on server and client.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Vanguard = require(ReplicatedStorage.Packages.Vanguard)
return Vanguard.CreateComponent({
Name = "TaggedButton",
Tag = "VanguardButton",
Construct = function(self)
if not self.Instance:IsA("GuiButton") then
return
end
self.Cleaner:Connect(self.Instance.Activated, function()
self.Logger:Info(`Activated {self.Instance:GetFullName()}`)
end)
end,
})
Component instance objects receive Instance, Cleaner, Component, Vanguard, and Logger.
Classes
Vanguard classes have constructors, inheritance, callable class tables, and runtime IsA checks. CreateClass registers the class in the current server or client runtime.
local Entity = Vanguard.CreateClass({
Name = "Entity",
Constructor = function(self, id)
self.Id = id
end,
})
function Entity:GetId()
return self.Id
end
local PlayerEntity = Vanguard.CreateClass({
Name = "PlayerEntity",
Extends = Entity,
Constructor = function(self, _id, player)
self.Player = player
end,
})
local entity = PlayerEntity("player-1", game.Players.SomePlayer)
print(entity:GetId(), entity:IsA(Entity), entity:IsA("PlayerEntity"))
print(Vanguard.GetClass("PlayerEntity") == PlayerEntity)
Base constructors run before child constructors with the same arguments. Use Vanguard.RegisterClass(Vanguard.Util.Class.create(...)) for externally-created classes. Class modules can return either a class or a plain class definition and be loaded with AddClasses, AddClassesDeep, or the Classes bootstrap field. Classes may be registered and unregistered after startup because they do not participate in lifecycle ordering.
Classes can also separate public instance members, per-instance private state, and class-only static members:
local Wallet
Wallet = Vanguard.CreateClass({
Name = "Wallet",
Private = {
Balance = 0,
Constructor = function(_self, private, openingBalance)
private.Balance = openingBalance
end,
},
Public = {
GetBalance = function(_self, private)
return private.Balance
end,
},
Static = {
Currency = "Credits",
Open = function(openingBalance)
return Wallet(openingBalance)
end,
},
})
local wallet = Wallet.Open(100)
print(wallet:GetBalance()) -- 100
print(wallet.Balance) -- nil
print(Wallet.Currency) -- Credits
print(wallet.Currency) -- nil
Grouped public functions receive their owning class's private state as the second argument. Private defaults are cloned per instance, private methods are callable only through that injected state, and inherited public methods retain access to the base class's private state. Legacy top-level methods continue to use the original (self, ...) signature.
Plugin Developer API
Vanguard 0.1.15 adds a first-class plugin registry for framework extensions, diagnostics, Studio tooling, and project-specific conventions. Plugins load before classes, services, controllers, and components when passed through Bootstrap.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Vanguard = require(ReplicatedStorage.Packages.Vanguard)
return Vanguard.CreatePlugin({
Name = "DiagnosticsPlugin",
Version = "1.0.0",
Runtime = "Shared",
Priority = 10,
VanguardInit = function(self, context)
context.Logger:Info(`Loaded for {context.Runtime}`)
end,
Hooks = {
ServiceRegistered = function(context, payload)
context.Logger:Debug(`Service: {payload.Name}`)
end,
NetworkRejected = function(context, payload)
context.Logger:Warn(payload.Rejection.Code, payload.Context.RemoteName)
end,
},
})
Plugin fields:
Nameis required and unique per runtime.Runtimecan beServer,Client, orShared.Prioritycontrols plugin lifecycle order; higher numbers run first.DependsOnlists plugin names that must run before this plugin.Strict = truemakes that plugin fail startup when one of its hooks throws.Hookscan handlePluginRegistered,PluginUnregistered,ClassRegistered,ServiceRegistered,ControllerRegistered,ComponentRegistered,RemoteRegistered,RemoteDiscovered,NetworkRejected,BeforeStart, andAfterStart.
Global plugin behavior can be configured at startup:
Vanguard.Start({
Plugins = {
Strict = false,
LogHooks = false,
},
})
Use AddPlugins, AddPluginsDeep, GetPlugin, GetPlugins, HasPlugin, and UnregisterPlugin for manual plugin control before startup.
Documented Errors
Framework-owned failures now include a stable code and a direct documentation link:
[Vanguard VG-NET-001] Vanguard network protocol mismatch (client 2, server 1)
Docs: https://twrblxdevs.github.io/vanguard-docs/errors/#vg-net-001
Use Vanguard.Error or Vanguard.Util.Error to create the same structured errors in game code:
local message = Vanguard.Error.format("VG-CORE-001", "Inventory configuration is invalid")
warn(message)
Network rejection names such as INVALID_PAYLOAD and UNAUTHENTICATED remain stable and now link to their corresponding error entry.
Remotes
Vanguard.CreateSignal() creates a reliable two-way remote event.
Server methods: :Fire(player, ...), :FireAll(...), :FireExcept(player, ...), :FireWhere(predicate, ...), :Connect(function(player, ...) end).
Client methods: :Fire(...), :Connect(function(...) end).
Vanguard.CreateUnreliableSignal() uses UnreliableRemoteEvent when available and falls back to RemoteEvent.
Vanguard.CreateProperty(initialValue) creates a server-owned replicated value.
Server methods: :Set(value), :SetFor(player, value), :ClearFor(player), :Get(player), :Observe(callback).
Client methods: :Get(), :Observe(callback).
Remote property methods support both colon and dot calls, so Property:Set(true) and Property.Set(true) both work.
Vanguard.CreateReplicator(initialState) creates a server-owned replicated state tree for larger, structured state.
local MatchService = Vanguard.CreateService({
Name = "MatchService",
Client = {
State = Vanguard.CreateReplicator({
Phase = "Lobby",
Score = {
Red = 0,
Blue = 0,
},
}),
},
})
function MatchService:VanguardStart()
self.Client.State:SetPath("Score.Red", 1)
self.Client.State:Patch({
Phase = "Active",
})
end
Server methods: :Get(player?, path?), :Set(value), :SetPath(path, value), :Patch(patch), :SetFor(player, value), :SetPathFor(player, path, value), :PatchFor(player, patch), :ClearFor(player), :Observe(callback).
Client methods: :Get(path?), :Observe(callback), :ObservePath(path, callback).
Paths can be dot strings like "Score.Red" or arrays like { "Inventory", slotId }. Replicators are server-owned; clients can read and observe state, while all mutation stays on the server.
Network Security
Every client-to-server remote function, signal, and property read passes through Vanguard's network guard. Rules can rate-limit requests, validate payload shape, authenticate the player, and verify whether that player may perform the requested action.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Vanguard = require(ReplicatedStorage.Packages.Vanguard)
local Validator = require(Vanguard.Util.Validator)
local PurchaseService = Vanguard.CreateService({
Name = "PurchaseService",
Client = {
Purchase = function(self, player, itemId, amount)
return self.Server:Purchase(player, itemId, amount)
end,
PurchaseRequested = Vanguard.CreateSignal(),
},
Network = {
Default = {
RateLimit = {
Limit = 30,
Window = 1,
},
},
Purchase = {
Validate = Validator.tuple(
Validator.string({ MinLength = 1, MaxLength = 64 }),
Validator.integer({ Min = 1, Max = 10 })
),
Authenticate = function(player, context)
return ProfileManager:IsLoaded(player), "Profile is not ready"
end,
Verify = function(player, context, itemId, amount)
return Catalog:CanPurchase(player, itemId, amount), "Purchase is not permitted"
end,
RateLimit = {
Limit = 5,
Window = 2,
},
},
},
})
Rules use the remote's Client key, so the same configuration works for functions, signals, and properties. A remote rule inherits Network.Default. Set an inherited field to false, such as RateLimit = false, to disable that field for one remote.
The server evaluates inbound requests in this order:
RateLimitlimits each player independently.Validate(...)checks only client-supplied arguments.Authenticate(player, context)verifies server-owned session or identity state.Verify(player, context, ...)authorizes the specific action and resources.
Configure authentication across every service at startup:
Vanguard.Start({
Network = {
Authenticate = function(player, context)
return not BanService:IsBanned(player), "Access denied"
end,
OnRejected = function(context, rejection)
SecurityMetrics:Record(context.Player, context.RemoteName, rejection.Code)
end,
LogRejected = true,
},
})
Global authentication and verification cannot be bypassed by a service rule. Rejected remote functions and property reads return errors such as [VanguardNetwork/INVALID_PAYLOAD]; rejected signals are dropped before service listeners run. Rejection logs are throttled per player, remote, and code.
Vanguard also stamps the remote container with its network protocol and server package version. Clients reject incompatible protocols before building service proxies and warn on package-version mismatches.
Protocol 2 keeps the protocol 1 _VanguardRemotes folder, service folders, RemoteFunction method transport, reliable or unreliable signal transport, remote-property _Get and _Changed children, server-supplied Player identity, guard ordering, and client compatibility handshake. It adds remote kind metadata and the Replicator remote kind so clients can distinguish state trees from legacy properties. Vanguard 0.1.14 remains the last protocol 1 release; Vanguard 0.1.15 is protocol 2.
local info = Vanguard.GetNetworkInfo()
-- Server only:
local stats = Vanguard.GetNetworkStats()
print(stats.Accepted, stats.Rejected, stats.ByCode.RATE_LIMITED)
Vanguard.ResetNetworkStats()
Validation is not authentication. Keep ownership, permissions, inventory, currency, and session state on the server and check them in Authenticate or Verify.
Validators
Vanguard.Util.Validator includes any, type, robloxType, string, number, integer, boolean, literal, optional, oneOf, array, map, shape, instance, custom, and tuple validators. Shapes reject unknown keys by default, arrays reject non-sequential keys, and numbers reject NaN and infinity by default.
Utilities
local Cache = require(Vanguard.Util.Cache)
local Class = require(Vanguard.Util.Class)
local Cleaner = require(Vanguard.Util.Cleaner)
local Component = require(Vanguard.Util.Component)
local Error = require(Vanguard.Util.Error)
local Logger = require(Vanguard.Util.Logger)
local Math = require(Vanguard.Util.Math)
local NetworkGuard = require(Vanguard.Util.NetworkGuard)
local Promise = require(Vanguard.Util.Promise)
local RateLimiter = require(Vanguard.Util.RateLimiter)
local Signal = require(Vanguard.Util.Signal)
local Switch = require(Vanguard.Util.Switch)
local Validator = require(Vanguard.Util.Validator)
Vanguard.CreateLogger("Scope") creates a scoped logger using the current LogLevel.
Cache
Caches support optional TTL expiry, LRU capacity limits, nil values, lazy population, and injectable clocks for testing.
local profileCache = Vanguard.CreateCache({
DefaultTTL = 60,
MaxSize = 100,
})
local profile, wasCached = profileCache:GetOrSet(player.UserId, function(userId)
return loadProfile(userId)
end)
local cachedValue, found = profileCache:Get(player.UserId)
profileCache:Remove(player.UserId)
Get, Peek, and Remove return value, found, so a cached nil value remains distinguishable from a miss. GetOrSet returns value, wasCached. Peek does not refresh LRU recency.
Rate Limiter
The per-key rolling-window limiter is useful at remote and gameplay boundaries. The second return value is the retry delay in seconds.
local actionLimiter = Vanguard.CreateRateLimiter({
Limit = 10,
Window = 1,
WeakKeys = true,
})
local allowed, retryAfter = actionLimiter:Check(player)
if not allowed then
return nil, `Try again in {retryAfter} seconds`
end
Math
Vanguard.Math and Vanguard.Util.Math provide common gameplay-number helpers:
local damagePercent = Vanguard.Math.inverseLerp(0, maxHealth, currentHealth)
local screenX = Vanguard.Math.map(worldX, -100, 100, 0, 1920, true)
local stepped = Vanguard.Math.moveTowards(currentSpeed, targetSpeed, acceleration * deltaTime)
local slot = Vanguard.Math.snap(rawSlot, 5)
The module also includes round, lerp, approximatelyEqual, wrap, pingPong, smoothstep, smootherstep, and average.
Switch
Vanguard.Switch provides explicit JavaScript-style case dispatch without fall-through:
local nextState = Vanguard.Switch.new(state)
:Case("Paused", "Playing")
:Cases({ "Playing", "Resuming" }, function(current, suffix)
return current .. suffix
end)
:Default("Idle")
:Run("State")
Use Switch.match(value, cases, defaultResult, ...) for concise table-based dispatch. Cases use Luau equality and execute in declaration order.
Upcoming Features
Vanguard's next development phases are tracked in the public roadmap. Vanguard 0.1.15 adds the Plugin Developer API, protocol 2, and the first replicated state tree API. Following priorities include:
- a verified Roblox Toolbox release for non-Wally installation;
- additional utility modules selected around common production needs;
- expanded documentation with recipes, examples, and architecture references;
- Studio tooling for generation, validation, diagnostics, and protocol inspection;
- a clearer community contribution process with RFCs and scoped starter issues.
Roadmap items are directional until assigned to a release. Compatibility and security work take priority over delivery dates.
Build
Build the package module:
rojo build release.project.json -o "releases/Vanguard-0.1.15.rbxm"
Package Details
Install command (Click to copy)
Version
0.1.15
License
MIT
Safe for commercial use
Automated license review — not legal advice.
