The site is under maintenance, so logging in is temporarily unavailable. All resources and the MCP still work.

AI Skills

Game frameworks - Knit and Nevermore

By Roblox Vibecoders · roblox-frameworks

Use when a Roblox project uses or is choosing a game framework - Knit (Knit.CreateService, controllers, KnitServer/KnitClient) or Nevermore (@quenty npm packages, the loader line, ServiceBag, Binder, BaseObject, Maid, Rx/Brio/Blend) - or when adding a service, controller or tagged-instance class to one. Covers how each boots, their real APIs, and which to pick.

Install it

Terminal
mkdir -p ~/.claude/skills/roblox-frameworks && curl -fsSL https://robloxvibecoders.com/skills/roblox-frameworks/SKILL.md -o ~/.claude/skills/roblox-frameworks/SKILL.md

Skills in ~/.claude/skills work in every project. Put them in your project's .claude/skills folder to share them with your team.

What your AI reads

SKILL.md
---
name: roblox-frameworks
description: Use when a Roblox project uses or is choosing a game framework - Knit (Knit.CreateService, controllers, KnitServer/KnitClient) or Nevermore (@quenty npm packages, the loader line, ServiceBag, Binder, BaseObject, Maid, Rx/Brio/Blend) - or when adding a service, controller or tagged-instance class to one. Covers how each boots, their real APIs, and which to pick.
---

# Game frameworks: Knit and Nevermore

A framework decides how the game boots: one server entry script and one client entry script load every system in a
fixed order (init everything, then start everything). The two you will meet most are **Knit** and **Nevermore**.

## First: detect, don't switch

- `Knit = "sleitnick/knit@..."` in `wally.toml`, modules calling `Knit.CreateService` → **Knit project**.
- `package.json` with `@quenty/*` dependencies, and files whose first line is
  `local require = require(script.Parent.loader).load(script)` → **Nevermore project**.
- Neither → don't add one unasked. A single server Script and a single client Script that require ModuleScripts
  with `init`/`start` functions is enough (see the `roblox-script-run-context` skill).

Never mix frameworks or migrate one to the other unless the user asks.

## Which one

| | Knit | Nevermore |
|---|---|---|
| Status | Archived. The README says "No Longer Maintained" | Actively developed monorepo |
| Install | Wally: `sleitnick/knit@1.7.0` | npm/pnpm: `@quenty/<package>` |
| Networking | Built in (the service's `Client` table) | Separate packages (`@quenty/remoting`) or your own |
| Typing | Weak; the archival note says Knit "cannot fully benefit from types" | `--!strict` everywhere |
| Pick it when | The game already uses Knit | Large game, team comfortable with npm, Rojo and DI |

For a **new** game, don't start on Knit. Use plain modules plus Packet for networking (`roblox-packet-networking`), or
Nevermore if the user wants its libraries.

## Knit

```toml
[dependencies]
Knit = "sleitnick/knit@1.7.0" # pulls in sleitnick/comm and evaera/promise
```

```luau
-- ServerScriptService/Main.server.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local Knit = require(ReplicatedStorage.Packages.Knit)

Knit.AddServices(ServerScriptService.Services) -- requires every ModuleScript child
Knit.Start():catch(warn)
```

```luau
-- ServerScriptService/Services/CoinService.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Knit = require(ReplicatedStorage.Packages.Knit)

local CoinService = Knit.CreateService({
	Name = "CoinService",
	Client = {
		Coins = Knit.CreateProperty(0), -- RemoteProperty
		Rewarded = Knit.CreateSignal(), -- RemoteSignal
	},
})

function CoinService:KnitInit()
	self._coins = {} :: { [Player]: number }
end

function CoinService:KnitStart()
	-- every service has run KnitInit; safe to use Knit.GetService("OtherService") here
end

function CoinService:Add(player: Player, amount: number)
	local coins = (self._coins[player] or 0) + amount
	self._coins[player] = coins
	self.Client.Coins:SetFor(player, coins)
	self.Client.Rewarded:Fire(player, amount)
end

-- Callable from clients. The first argument is always the calling player.
function CoinService.Client:GetCoins(player: Player): number
	return self.Server._coins[player] or 0
end

return CoinService
```

```luau
-- Client: Knit.AddControllers(folder) then Knit.Start():catch(warn), same as the server.
local CoinController = Knit.CreateController({ Name = "CoinController" })

function CoinController:KnitStart()
	local CoinService = Knit.GetService("CoinService")
	CoinService.Coins:Observe(function(coins)
		print("Coins:", coins)
	end)
	CoinService:GetCoins():andThen(function(coins) -- client methods return Promises by default
		print(coins)
	end)
end

return CoinController
```

Knit rules:

- `Knit.GetService` errors before `Knit.Start()`. Call it inside `KnitInit`/`KnitStart`, never at module top level.
- Services and controllers must be created before `Knit.Start()`.
- A service is visible to clients only if its `Client` table has methods, signals or properties.
- `Knit.Start({ ServicePromises = false })` on the client makes client methods return values instead of Promises.
- Server signal API: `:Fire(player, ...)`, `:FireAll(...)`, `:FireExcept(player, ...)`, `:Connect(fn(player, ...))`.
  Property API: `:Set(v)`, `:SetFor(player, v)`, `:Get()`, `:GetFor(player)`. Client property: `:Get()`, `:Observe(fn)`.
- Knit does not validate anything. Type-check every argument of every `Client` method and signal on the server.

## Nevermore

Packages come from npm, not Wally. The game repo has a `package.json`; Rojo syncs `node_modules`.

```bash
npm install -g @quenty/nevermore-cli
nevermore init            # scaffolds a game (Rojo project, src/modules, src/scripts, pnpm)
pnpm install @quenty/binder   # add a package; the npm name is the package.json "name"
```

Every module starts with the loader line, then requires by module name, not path:

```luau
local require = require(script.Parent.loader).load(script)

local Maid = require("Maid")
```

Boot scripts (as generated by the CLI):

```luau
-- src/scripts/Server/ServerMain.server.lua
local ServerScriptService = game:GetService("ServerScriptService")

local root = ServerScriptService.MyGame
local loader = root:FindFirstChild("LoaderUtils", true).Parent
local require = require(loader).bootstrapGame(root)

local serviceBag = require("ServiceBag").new()
serviceBag:GetService(require("MyGameService"))
serviceBag:Init()
serviceBag:Start()
```

```luau
-- src/scripts/Client/ClientMain.client.lua
local loader = game:GetService("ReplicatedStorage"):WaitForChild("MyGame"):WaitForChild("loader")
local require = require(loader).bootstrapGame(loader.Parent)

local serviceBag = require("ServiceBag").new()
serviceBag:GetService(require("MyGameServiceClient"))
serviceBag:Init()
serviceBag:Start()
```

### ServiceBag services

```luau
local require = require(script.Parent.loader).load(script)

local Maid = require("Maid")

local RoundService = {}
RoundService.ServiceName = "RoundService"

function RoundService:Init(serviceBag)
	assert(not self._serviceBag, "Already initialized")
	self._serviceBag = assert(serviceBag, "No serviceBag")
	self._maid = Maid.new()

	-- Dependencies are retrieved (and initialized) through the bag, in Init
	self._doorBinder = self._serviceBag:GetService(require("Door"))
end

function RoundService:Start()
	-- every service is initialized; begin behavior
end

function RoundService:Destroy()
	self._maid:DoCleaning()
end

return RoundService
```

- `Init` and `Start` must not yield. Put waiting work in a spawned thread or a Promise.
- Get services only through the bag. Calling methods on the raw required module gives an uninitialized copy.
- No new services can be added after `serviceBag:Init()` finishes, so retrieve them all inside some `Init`.
- New features: create the service, then `GetService` it from the top-level game service's `Init`.

### Binder + BaseObject (a class per tagged Instance)

```luau
local require = require(script.Parent.loader).load(script)

local BaseObject = require("BaseObject")
local Binder = require("Binder")

local Door = setmetatable({}, BaseObject)
Door.ClassName = "Door"
Door.__index = Door

function Door.new(part: BasePart, serviceBag)
	local self = setmetatable(BaseObject.new(part), Door)
	self._serviceBag = assert(serviceBag, "No serviceBag")

	self._maid:GiveTask(part.Touched:Connect(function(hit)
		-- self._obj is the tagged Instance
	end))

	return self
end

return Binder.new("Door", Door)
```

A Binder is itself a service (`ServiceName` is the tag plus "Binder"): `serviceBag:GetService(require("Door"))` initializes
it, passes the ServiceBag into every `Door.new`, and starts binding on `serviceBag:Start()`. Tag an Instance with
`CollectionService:AddTag(part, "Door")` or `binder:Tag(part)`; get the object with `binder:Get(part)`; listen with
`binder:GetClassAddedSignal()`. Destroying the class happens automatically when the tag or Instance goes away.

### Maid, Rx, Brio, Blend

- **Maid**: `maid:GiveTask(connectionOrInstanceOrFn)`, named slots `maid._thing = x` (replacing cleans the old one),
  `maid:DoCleaning()`. `BaseObject` gives every class `self._maid` and `self._obj`, and `:Destroy()` cleans it.
- **Rx**: observables with `:Pipe({ Rx.map(fn), Rx.where(fn) })` and `:Subscribe(fn)`. Give the subscription to a maid.
- **Brio**: a value with a lifetime (`:GetValue()`, `:IsDead()`, `:GetDiedSignal()`). `binder:ObserveBrio(inst)` emits
  them so you never use a destroyed object.
- **Blend**: declarative UI. `Blend.New "Frame" { ... }`, `Blend.State(v)`, `Blend.Computed(...)`;
  `maid:GiveTask(tree:Subscribe())` mounts it.

The local reference for real signatures is each package's source (`src/<package>/src/Shared/*.lua` in the
NevermoreEngine repo). Read it before using an API you haven't seen in the project.

## Mistakes to avoid

- Adding Knit to a new game, or adding a second framework to a game that has one.
- Knit: calling `Knit.GetService` at the top of a module, or creating services after `Knit.Start()`.
- Knit: treating client method results as plain values when `ServicePromises` is on.
- Nevermore: forgetting the loader line, or requiring packages by path instead of `require("Name")`.
- Nevermore: requiring a service and calling it without `serviceBag:GetService`, or yielding in `Init`/`Start`.
- Nevermore: editing files inside `node_modules`. Change versions in `package.json` and reinstall.
- Both: trusting arguments that came from a client. Frameworks move data; validation is still your job.