[Open Source] V2.2.0 Nextinel Framework - modular server/client setup

Hey everyone!

Ive been working on my own framework for a while and finally decided to share it. I got tired of rewriting the same stuff (networking, player data, module loading) for every new project, so I built this for myself. Maybe someone else will find it useful

Its pretty simple. You split your code into modules with lifecycle hooks, and the framework handles the rest: dependency injection, player/character events, heartbeat updates, and network contracts

This is a solo project built from scratch to solve my own workflow needs. Its not a clone of anything - just my personal take on a lightweight framework

Quick Start: Install the Nextinel Builder Plugin to auto-generate the entire folder structure and typed boilerplate modules in one click


Example (server code):

So instead of writing manually

local players = game:GetService("Players")

function CharacterAdded(character: Model)
	local humanoid = character:WaitForChild("Humanoid")
	local root = character:WaitForChild("HumanoidRootPart")
	
	...
end

function PlayerAdded(player: Player)
	player.CharacterAdded:Connect(CharacterAdded)
end

players.PlayerAdded:Connect(PlayerAdded)

You can write it that way:

local replicatedStorage = game:GetService("ReplicatedStorage")
local Types = require(replicatedStorage.Modules.Global.Types)

return {
	characterLoaded = function(character, humanoid, root, player)
		...
	end,
} :: Types.ServerModule

No need to call WaitForChild manually - the framework waits for the Humanoid and RootPart to exist before calling this hook


Under the hood

  • Nextinel Builder: Studio plugin for one-click setup and generating typed modules
  • Type Safety: Built-in Types.ServerModule and Types.ClientModule for full IntelliSense on all hooks
  • Error Tracebacks: Filtered xpcall tracebacks that point directly to your module, hiding framework internals
  • Heartbeat buckets: To update modules across 3 buckets so it doesn’t lag
  • Jolt API: For networking, with Network contracts for runtime type validation
  • DataStoreService: Built on ProfileStore with Migrator, typed profiles, and :observe() methods
  • HangWatcher: Warns if a module’s init takes longer than 10s
  • Dependency Injection: Modules declare dependencies, resolved with priority

Why Nextinel?

Unlike heavier frameworks, Nextinel gives you a lightweight base. You get the routine handled for you, but you keep full control over your architecture. No bloat, no magic

Known Limitations

Note: Dependencies are still string-based for simplicity (dependencies = {"ModuleName"} ). This means dependingServices won’t have autocomplete inside init(), but all lifecycle hooks are now fully typed

Full API Reference


Server module hooks:

return {
    dependencies = {"ModuleName"}, -- optional
    
    init = function(dependingServices) end,
    update = function(deltaTime) end,
    
    playerAdded = function(player) end,
    playerDataLoaded = function(player, profile) end,
    playerRemoving = function(player) end,
    
    characterAdded = function(character, player) end,
    characterLoaded = function(character, humanoid, root, player) end,
    humanoidAdded = function(humanoid, player) end,
    characterRemoved = function(character, player) end,
    
    network = {
        channel = "ChannelName",
        shouldCheckExtrafields = false, -- optional (default: true)
        contract = {[1] = "string", [2] = "number"}, -- optional
        onEvent = function(name, count) end,
    },
}

Client module hooks:

local channels = {}

--[[
to call fireServer:
channels.ChannelName:fireServer(...)
]]

return {
    dependencies = {"ModuleName"}, -- optional
    
    init = function(dependingServices) end,
    asyncInit = function() end, -- runs asynchronously (wrapped in task.spawn)
    update = function(deltaTime) end,
    clientReady = function() end,
    
    characterAdded = function(character) end,
    characterLoaded = function(character, humanoid, root) end,
    humanoidAdded = function(humanoid) end,
    characterRemoved = function(character) end,
    
    network = {
        channel = "ChannelName",
        shouldCheckExtrafields = false, -- optional (default: true)
        contract = {[1] = "string", [2] = "number"}, -- optional
        onEvent = function(name, count) end,
    },
    
    setupFireServer = {
        channelsNames = {"ChannelName"},
        setupRemote = function(name, network)
            channels[name] = network
        end,
    },
}

Note: asyncInit is called after init but runs asynchronously, so it doesn’t block the main thread


Updates (Posts)

v1.1.0 Network changes: Click
v1.1.1 Logger & Types: Click
v2.0.0 DataStore Module Rework: Click
v2.1.0 HangWatcher, Priorities & Profile Methods: Click
v2.2.0 Nextinel Builder & Type Safety: Click


GitHub

I put full docs and installation instructions in the README. Download link is in Releases
GitHub Repo


If you find any bugs or have ideas, let me know! Still working on it.

2 Likes

And this comes into the point Sleitnick made about cyclic dependencies.
The dependency table will not warn you for it as there will be no intellisense, also another point Sleitnick made about disconintuing knit.

Modules work as “Services” or “Controllers” out of the box.

Anyways, provide any benefits or anything new?

honestly I dont know much about sleitnick, just made this thing for myself

yeah, you’re right about intellisense tho. With string dependencies you dont get autocomplete. Thats the trade-off

about benefits, so there is heartbeat buckets, network contracts (type validation), DataStoreService module built on ProfileStore and a version checker

also about dependencies cycling: my DependencyResolver catches circular dependencies so it just wont freeze forever

Well, imo, losing intellisense is not a good trade-off, especially with roblox updating it since Sleitnick made knit, it’s 10x better.

And if your networker uses buffers for everything (if it does) then that’s pointless, as buffers are practically useless for small amount of data, but I assume its only for types (Which you can just do argument: number)

1 Like

fair enough. So about intellisense - thats the main downside of string dependencies. I chose it cuz it was simpler for me to work with that but I might look into type-safe DI later

about buffers: I think theres a misunderstanding. Jolt handles the actual networking under the hood. My framework just uses contract for runtime type validation, so a client cant send string where number is expected

appreciate the feedback though, still learning

v1.1.0 Network changes


GitHub release: Release v1.1.0

Breaking changes:

  • network.remote → network.channel
  • setupFireServer.remoteNames → setupFireServer.channelsNames
  • Removed Remotes Folder in ReplicatedStorage
  • Network now uses Jolt (bundled with the framework at ReplicatedStorage.Modules.Global.Jolt)

Not changed:

  • contract
  • onEvent

How to use on server

Instead of getting remotes from Remotes Folder, now Jolt module is required to fire client:

local replicatedStorage = game:GetService("ReplicatedStorage")

local jolt = require(replicatedStorage.Modules.Global.Jolt)

local inventoryChannel = jolt.Server("Inventory")

return {
	playerAdded = function(player)
		-- Fire to a specific player (server -> client)
		inventoryChannel:Fire(player, "hello", 123)
	end,
	
	update = function(dt)
		-- Fire to all clients
		inventoryChannel:FireAll("tick", dt)
	end,
}

v1.1.1 Logger & Types


GitHub release: Release v1.1.1

New Shared Modules in ReplicatedStorage

  • Logger - logger.register(src), this:info(), this:warn(), this:error()
  • Types - Shared type definitions for Client and Server modules

Notes

No breaking changes. Existing code continues to work as-is

v2.0.0 DataStore Module Rework


GitHub release: Release: v2.0.0

Breaking Changes

  • Now you can get profile type from dataStoreService.profile (exported type)
  • Reworked methods: profile:add(key, delta) and profile:set(key, value) instead of dataStoreService.add(player, key, delta) and dataStoreService.set(player, key, value)
  • Added paths for IN_PLAYER entries instead of parent
  • getProfile(player) now returns a wrapped profile object, not profile.Data. Access raw data via profile.Data.<key> or use profile:add() / profile:set()
  • And a few little fixes

Examples

Before:

local dataStoreService = require("./../Global/DataStoreService")

return {
	update = function()
		for _, plr in game:GetService("Players"):GetPlayers() do
			local profile = dataStoreService.getProfile(plr)
			
			if not profile then continue end
			
			profile.Coins += 1
			dataStoreService.updateData(plr) -- to update leaderstats and other folders in player Instance
		end
	end,
	
	playerDataLoaded = function(plr: Player, profile: any)
		dataStoreService.set(plr, "Coins", 100)
		
		dataStoreService.add(plr, "Coins", math.random(-100, 100))
	end,
}

After:

local dataStoreService = require("./../Global/DataStoreService")

return {
	update = function()
		for _, plr in game:GetService("Players"):GetPlayers() do
			local profile = dataStoreService.getProfile(plr)
			
			if not profile then continue end
			
			profile.Data.Coins += 1
			profile:updateFolders() -- to update leaderstats and other folders in player Instance
		end
	end,
	
	playerDataLoaded = function(plr: Player, profile: dataStoreService.profile)
		profile:set("Coins", 100)
		
		profile:add("Coins", math.random(-100, 100))
	end,
}

v2.1.0 HangWatcher, Priorities & Profile Methods


GitHub release: Release: v2.1.0

Added

  • HangWatcher - warns if a module’s init takes longer than 10 seconds. Configurable per module with skipHangWatch = true
  • Priorities - control init order with priority = -1 (lower = earlier). Dependencies always take precedence over priority
  • Profile methods:
    • profile:decrease(key, delta) / profile:decreaseAt(path, key, delta) - shorthand for negative add
    • profile:observe(key, callback) / profile:observe(path, key, callback) - subscribe to field changes. Only number, string, boolean types are supported. Triggers only on :add(), :addAt(), :set(), :setAt(), :decrease(), :decreaseAt()
    • profile:addAt(path, key, delta) / profile:setAt(path, key, value) - modify nested fields without leaving the Data table
  • DataStoreService functions:
    • .tryGetProfile(player) - non-yielding, returns nil if not loaded
    • .awaitProfile(player) - explicit yielding alias of .getProfile(player)
    • .getProfiles() - returns all loaded profiles
  • skipHangWatch - per-module field to disable hang warnings
  • shouldCheckExtrafields - optional flag in network table for the validator. Default is true. Set to false to allow extra fields in the payload

Fixed

  • DependencyResolver now uses Kahn’s algorithm with priority as a tie-breaker

Notes

  • asyncInit runs in parallel via task.spawn and is not affected by priority or dependency order
  • No breaking changes. Existing modules work as-is

Demo Place & Examples

You can see it in GitHub release post: Click

v2.2.0 Nextinel Builder & Type Safety


GitHub release: Release: v2.2.0

Added

  • Nextinel Builder Plugin: Auto-generates folder structure and typed boilerplate modules. Download here
  • Full Type Support: Added Types.ServerModule and Types.ClientModule. You can now use :: Types.ServerModule for full IntelliSense on lifecycle hooks

Improved

  • Error Tracebacks: Switched from pcall to xpcall across the entire framework. When an error occurs in any hook (not just init), the stack trace is filtered to remove internal framework calls. You now see exactly which line in your module caused the crash
  • Logger: Cleaned up internal warnings and added better filtering for stack traces

Notes

  • No breaking changes. Existing modules work as-is
  • Highly recommend updating to get the new Plugin and Type safety features