{"id":"twrblxdevs/vanguard","name":"vanguard","scope":"twrblxdevs","platform":"roblox","description":"A dependency-free Knit-inspired application framework for Roblox.","version":"0.1.15","latest":"0.1.15","versions":["0.1.0","0.1.1","0.1.2","0.1.3","0.1.4","0.1.5","0.1.6","0.1.7","0.1.8","0.1.9","0.1.10","0.1.13","0.1.14","0.1.15"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"533b0aa41eac5d580c1c4ea481e6167aaa87bd3493e798f5c5f595c4e4d2847b","likes":0,"downloads":0,"install":"forest install twrblxdevs/vanguard","url":"https://forest.dev/p/roblox/twrblxdevs/vanguard","files":"https://api.forest.dev/ai/package/roblox/twrblxdevs/vanguard/files","readme":"# Vanguard\r\n\r\nVanguard 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.\r\n\r\n## Documentation\r\n\r\nThe complete documentation lives at [twrblxdevs.github.io/vanguard-docs](https://twrblxdevs.github.io/vanguard-docs/).\r\n\r\n- [Getting Started](https://twrblxdevs.github.io/vanguard-docs/getting-started/)\r\n- [Lifecycle](https://twrblxdevs.github.io/vanguard-docs/lifecycle/)\r\n- [Services](https://twrblxdevs.github.io/vanguard-docs/services/), [Controllers](https://twrblxdevs.github.io/vanguard-docs/controllers/), [Components](https://twrblxdevs.github.io/vanguard-docs/components/), and [Classes](https://twrblxdevs.github.io/vanguard-docs/classes/)\r\n- [Networking](https://twrblxdevs.github.io/vanguard-docs/networking/) and [Network Security](https://twrblxdevs.github.io/vanguard-docs/network-security/)\r\n- [Errors](https://twrblxdevs.github.io/vanguard-docs/errors/) and the complete [Utilities index](https://twrblxdevs.github.io/vanguard-docs/utilities/)\r\n- [API Reference](https://twrblxdevs.github.io/vanguard-docs/api-reference/), [Type System](https://twrblxdevs.github.io/vanguard-docs/type-system/), and [Troubleshooting](https://twrblxdevs.github.io/vanguard-docs/troubleshooting/)\r\n- [Roadmap](https://twrblxdevs.github.io/vanguard-docs/roadmap/) for upcoming platform, tooling, and community work\r\n- [Contributing](https://twrblxdevs.github.io/vanguard-docs/contributing/) for setup, commit conventions, testing, and pull request expectations\r\n\r\n## Install\r\n\r\nAdd Vanguard to a game's `wally.toml`:\r\n\r\n```toml\r\n[dependencies]\r\nVanguard = \"twrblxdevs/vanguard@0.1.15\"\n```\r\n\r\nInstall packages:\r\n\r\n```bash\r\nwally install\r\n```\r\n\r\nMap Wally packages into your Rojo project:\r\n\r\n```json\r\n{\r\n  \"name\": \"MyGame\",\r\n  \"tree\": {\r\n    \"$className\": \"DataModel\",\r\n    \"ReplicatedStorage\": {\r\n      \"Packages\": {\r\n        \"$path\": \"Packages\"\r\n      }\r\n    }\r\n  }\r\n}\r\n```\r\n\r\nRequire Vanguard:\r\n\r\n```lua\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\nlocal Vanguard = require(ReplicatedStorage.Packages.Vanguard)\r\n```\r\n\r\nFor 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.\r\n\r\nIf you need `WaitForChild`, keep the static type from the direct path:\r\n\r\n```lua\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\nlocal Packages = ReplicatedStorage:WaitForChild(\"Packages\")\r\nlocal Vanguard = require(Packages:WaitForChild(\"Vanguard\") :: ModuleScript) :: typeof(require(ReplicatedStorage.Packages.Vanguard))\r\n```\r\n\r\n## Types\r\n\r\nVanguard exports Luau types from the main module:\r\n\r\n```lua\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\nlocal Vanguard = require(ReplicatedStorage.Packages.Vanguard)\r\n\r\ntype Vanguard = Vanguard.Vanguard\r\ntype StartOptions = Vanguard.StartOptions\r\ntype Service = Vanguard.Service\r\ntype Controller = Vanguard.Controller\ntype Component = Vanguard.Component\ntype Class = Vanguard.Class\ntype PluginDefinition = Vanguard.PluginDefinition\ntype Cache = Vanguard.Cache\ntype Replicator = Vanguard.Replicator\ntype VanguardError = Vanguard.VanguardError\ntype MathUtil = Vanguard.MathUtil\r\ntype RateLimiter = Vanguard.RateLimiter\r\ntype NetworkRule = Vanguard.NetworkRule\r\ntype NetworkContext = Vanguard.NetworkContext\r\ntype NetworkRejection = Vanguard.NetworkRejection\r\ntype Logger = Vanguard.Logger\r\ntype Promise = Vanguard.Promise\r\ntype RemoteSignal = Vanguard.RemoteSignal\r\ntype RemoteProperty = Vanguard.RemoteProperty\r\ntype Switch = Vanguard.Switch\r\n```\r\n\r\nFor service/controller-specific shapes, intersect your own fields with Vanguard's base types:\r\n\r\n```lua\r\ntype InventoryService = Vanguard.Service & {\r\n\tClient: {\r\n\t\tInventoryChanged: Vanguard.RemoteSignal,\r\n\t\tGetItems: (self: any, player: Player) -> { string },\r\n\t},\r\n\r\n\tGetItems: (self: any, player: Player) -> { string },\r\n}\r\n```\r\n\r\nUtility modules export types too:\r\n\r\n```lua\r\nlocal Promise = require(Vanguard.Util.Promise)\r\nlocal Cleaner = require(Vanguard.Util.Cleaner)\r\n\r\ntype Promise = Promise.Promise\r\ntype Cleaner = Cleaner.Cleaner\r\n```\r\n\r\n## Layout\r\n\r\nThis repository is package-only now:\r\n\r\n```text\r\nsrc\r\n  shared\r\n    Vanguard\r\n      init.luau\r\n      VanguardServer.luau\r\n      VanguardClient.luau\r\n      Util\r\n```\r\n\r\n`default.project.json` intentionally points directly at `src/shared/Vanguard` so Wally publishes a single module package.\r\n\r\n## Bootstrapping\r\n\r\nServer:\r\n\r\n```lua\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\nlocal Vanguard = require(ReplicatedStorage.Packages.Vanguard)\r\n\r\nVanguard.Bootstrap({\n\tServices = script.Parent.Services,\n\tComponents = script.Parent.Components,\n\tClasses = script.Parent.Classes,\n\tPlugins = script.Parent.Plugins,\n\tOptions = {\n\t\tLogLevel = \"info\",\n\t},\n}):catch(warn)\r\n```\r\n\r\nClient:\r\n\r\n```lua\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\nlocal Vanguard = require(ReplicatedStorage.Packages.Vanguard)\r\n\r\nVanguard.Bootstrap({\n\tControllers = script.Parent.Controllers,\n\tComponents = script.Parent.Components,\n\tClasses = script.Parent.Classes,\n\tPlugins = script.Parent.Plugins,\n\tOptions = {\n\t\tLogLevel = \"info\",\n\t},\n}):catch(warn)\r\n```\r\n\r\nYou 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.\r\n\r\nServices 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.\r\n\r\n## Startup Logs\r\n\r\nVanguard logs startup by default at `info` level:\r\n\r\n```text\r\n[Vanguard] [INFO] Starting server v0.1.15 (3 services, 2 components)\n[Vanguard] [INFO] Server startup complete in 12ms\n[Vanguard] [INFO] Starting client v0.1.15 (4 controllers, 1 component)\n[Vanguard] [INFO] Client startup complete in 8ms\n```\r\n\r\nUse `debug` for individual services/controllers/components:\r\n\r\n```lua\r\nVanguard.Start({\r\n\tLogLevel = \"debug\",\r\n})\r\n```\r\n\r\nUse `warn` or `silent` to reduce framework output.\r\n\r\n## Update Checks\r\n\r\nOn the server, Vanguard checks the Wally index after startup and warns if a newer published version exists:\r\n\r\n```text\r\n[VANGUARD OUTDATED]\r\n============================================================\r\nYour installed Vanguard version is out of date.\r\nInstalled: 0.1.14\nLatest:    0.1.15\n...\r\n============================================================\r\n```\r\n\r\nThe 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.\r\n\r\nDisable the check:\r\n\r\n```lua\r\nVanguard.Start({\r\n\tCheckForUpdates = false,\r\n})\r\n```\r\n\r\nUse a custom package index URL:\r\n\r\n```lua\r\nVanguard.Start({\r\n\tUpdateCheckUrl = \"https://raw.githubusercontent.com/UpliftGames/wally-index/main/twrblxdevs/vanguard\",\r\n})\r\n```\r\n\r\n## Services\r\n\r\nServices 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.\r\n\r\n```lua\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\nlocal Vanguard = require(ReplicatedStorage.Packages.Vanguard)\r\n\r\nlocal InventoryService = Vanguard.CreateService({\r\n\tName = \"InventoryService\",\r\n\tPriority = 0,\r\n\r\n\tClient = {\r\n\t\tInventoryChanged = Vanguard.CreateSignal(),\r\n\t},\r\n})\r\n\r\nfunction InventoryService.Client:GetItems(player)\r\n\treturn self.Server:GetItems(player)\r\nend\r\n\r\nfunction InventoryService:GetItems(player)\r\n\treturn {}\r\nend\r\n\r\nfunction InventoryService:VanguardStart()\r\n\tself.Logger:Info(\"Ready\")\r\nend\r\n\r\nreturn InventoryService\r\n```\r\n\r\nLifecycle hooks:\r\n\r\n```lua\r\nfunction Service:VanguardInit()\r\n\t-- Runs after all services are registered.\r\nend\r\n\r\nfunction Service:VanguardStart()\r\n\t-- Runs after every VanguardInit finishes.\r\nend\r\n```\r\n\r\n`KnitInit` and `KnitStart` are supported for migration.\r\n\r\nServices 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.\r\n\r\n`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.\r\n\r\n## Controllers\r\n\r\nControllers run on the client. Controller modules can either call `Vanguard.CreateController(...)` or return a plain controller table; `AddControllers` and `AddControllersDeep` register returned tables automatically.\r\n\r\n```lua\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\nlocal Vanguard = require(ReplicatedStorage.Packages.Vanguard)\r\n\r\nlocal InventoryController = Vanguard.CreateController({\r\n\tName = \"InventoryController\",\r\n})\r\n\r\nfunction InventoryController:VanguardStart()\r\n\tlocal InventoryService = Vanguard.GetService(\"InventoryService\")\r\n\r\n\tInventoryService:GetItems():andThen(function(items)\r\n\t\tself.Items = items\r\n\tend):catch(function(err)\r\n\t\tself.Logger:Warn(err)\r\n\tend)\r\nend\r\n\r\nreturn InventoryController\r\n```\r\n\r\nClient service methods return promises by default. Use yielding calls instead with:\r\n\r\n```lua\r\nVanguard.Start({\r\n\tServicePromises = false,\r\n})\r\n```\r\n\r\n## Components\r\n\r\nComponents attach behavior to instances tagged with CollectionService. They work on server and client.\r\n\r\n```lua\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\nlocal Vanguard = require(ReplicatedStorage.Packages.Vanguard)\r\n\r\nreturn Vanguard.CreateComponent({\r\n\tName = \"TaggedButton\",\r\n\tTag = \"VanguardButton\",\r\n\r\n\tConstruct = function(self)\r\n\t\tif not self.Instance:IsA(\"GuiButton\") then\r\n\t\t\treturn\r\n\t\tend\r\n\r\n\t\tself.Cleaner:Connect(self.Instance.Activated, function()\r\n\t\t\tself.Logger:Info(`Activated {self.Instance:GetFullName()}`)\r\n\t\tend)\r\n\tend,\r\n})\r\n```\r\n\r\nComponent instance objects receive `Instance`, `Cleaner`, `Component`, `Vanguard`, and `Logger`.\r\n\r\n## Classes\r\n\r\nVanguard classes have constructors, inheritance, callable class tables, and runtime `IsA` checks. `CreateClass` registers the class in the current server or client runtime.\r\n\r\n```lua\r\nlocal Entity = Vanguard.CreateClass({\r\n\tName = \"Entity\",\r\n\r\n\tConstructor = function(self, id)\r\n\t\tself.Id = id\r\n\tend,\r\n})\r\n\r\nfunction Entity:GetId()\r\n\treturn self.Id\r\nend\r\n\r\nlocal PlayerEntity = Vanguard.CreateClass({\r\n\tName = \"PlayerEntity\",\r\n\tExtends = Entity,\r\n\r\n\tConstructor = function(self, _id, player)\r\n\t\tself.Player = player\r\n\tend,\r\n})\r\n\r\nlocal entity = PlayerEntity(\"player-1\", game.Players.SomePlayer)\r\nprint(entity:GetId(), entity:IsA(Entity), entity:IsA(\"PlayerEntity\"))\r\nprint(Vanguard.GetClass(\"PlayerEntity\") == PlayerEntity)\r\n```\r\n\r\nBase 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.\r\n\r\nClasses can also separate public instance members, per-instance private state, and class-only static members:\r\n\r\n```lua\r\nlocal Wallet\r\nWallet = Vanguard.CreateClass({\r\n\tName = \"Wallet\",\r\n\r\n\tPrivate = {\r\n\t\tBalance = 0,\r\n\t\tConstructor = function(_self, private, openingBalance)\r\n\t\t\tprivate.Balance = openingBalance\r\n\t\tend,\r\n\t},\r\n\r\n\tPublic = {\r\n\t\tGetBalance = function(_self, private)\r\n\t\t\treturn private.Balance\r\n\t\tend,\r\n\t},\r\n\r\n\tStatic = {\r\n\t\tCurrency = \"Credits\",\r\n\t\tOpen = function(openingBalance)\r\n\t\t\treturn Wallet(openingBalance)\r\n\t\tend,\r\n\t},\r\n})\r\n\r\nlocal wallet = Wallet.Open(100)\r\nprint(wallet:GetBalance()) -- 100\r\nprint(wallet.Balance) -- nil\r\nprint(Wallet.Currency) -- Credits\r\nprint(wallet.Currency) -- nil\r\n```\r\n\r\nGrouped 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.\n\n## Plugin Developer API\n\nVanguard `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`.\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal Vanguard = require(ReplicatedStorage.Packages.Vanguard)\n\nreturn Vanguard.CreatePlugin({\n\tName = \"DiagnosticsPlugin\",\n\tVersion = \"1.0.0\",\n\tRuntime = \"Shared\",\n\tPriority = 10,\n\n\tVanguardInit = function(self, context)\n\t\tcontext.Logger:Info(`Loaded for {context.Runtime}`)\n\tend,\n\n\tHooks = {\n\t\tServiceRegistered = function(context, payload)\n\t\t\tcontext.Logger:Debug(`Service: {payload.Name}`)\n\t\tend,\n\n\t\tNetworkRejected = function(context, payload)\n\t\t\tcontext.Logger:Warn(payload.Rejection.Code, payload.Context.RemoteName)\n\t\tend,\n\t},\n})\n```\n\nPlugin fields:\n\n- `Name` is required and unique per runtime.\n- `Runtime` can be `Server`, `Client`, or `Shared`.\n- `Priority` controls plugin lifecycle order; higher numbers run first.\n- `DependsOn` lists plugin names that must run before this plugin.\n- `Strict = true` makes that plugin fail startup when one of its hooks throws.\n- `Hooks` can handle `PluginRegistered`, `PluginUnregistered`, `ClassRegistered`, `ServiceRegistered`, `ControllerRegistered`, `ComponentRegistered`, `RemoteRegistered`, `RemoteDiscovered`, `NetworkRejected`, `BeforeStart`, and `AfterStart`.\n\nGlobal plugin behavior can be configured at startup:\n\n```lua\nVanguard.Start({\n\tPlugins = {\n\t\tStrict = false,\n\t\tLogHooks = false,\n\t},\n})\n```\n\nUse `AddPlugins`, `AddPluginsDeep`, `GetPlugin`, `GetPlugins`, `HasPlugin`, and `UnregisterPlugin` for manual plugin control before startup.\n\n## Documented Errors\n\r\nFramework-owned failures now include a stable code and a direct documentation link:\r\n\r\n```text\r\n[Vanguard VG-NET-001] Vanguard network protocol mismatch (client 2, server 1)\nDocs: https://twrblxdevs.github.io/vanguard-docs/errors/#vg-net-001\n```\r\n\r\nUse `Vanguard.Error` or `Vanguard.Util.Error` to create the same structured errors in game code:\r\n\r\n```lua\r\nlocal message = Vanguard.Error.format(\"VG-CORE-001\", \"Inventory configuration is invalid\")\r\nwarn(message)\r\n```\r\n\r\nNetwork rejection names such as `INVALID_PAYLOAD` and `UNAUTHENTICATED` remain stable and now link to their corresponding error entry.\r\n\r\n## Remotes\r\n\r\n`Vanguard.CreateSignal()` creates a reliable two-way remote event.\r\n\r\nServer methods: `:Fire(player, ...)`, `:FireAll(...)`, `:FireExcept(player, ...)`, `:FireWhere(predicate, ...)`, `:Connect(function(player, ...) end)`.\r\n\r\nClient methods: `:Fire(...)`, `:Connect(function(...) end)`.\r\n\r\n`Vanguard.CreateUnreliableSignal()` uses `UnreliableRemoteEvent` when available and falls back to `RemoteEvent`.\r\n\r\n`Vanguard.CreateProperty(initialValue)` creates a server-owned replicated value.\r\n\r\nServer methods: `:Set(value)`, `:SetFor(player, value)`, `:ClearFor(player)`, `:Get(player)`, `:Observe(callback)`.\r\n\r\nClient methods: `:Get()`, `","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/twrblxdevs/roblox/vanguard/0.1.15/readme"}