> ## Documentation Index
> Fetch the complete documentation index at: https://flux-docs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Schemas

> Declare a fixed wire layout to drop type tags and narrow number widths.

By default Flux encodes each value with a one-byte type tag followed by its data, so
the receiver can decode anything without prior agreement. A schema replaces that with a
fixed, agreed-upon layout: no tags, and every number written at exactly the width you
declare.

```luau theme={null}
local T = Flux.Types

local Move = Flux.Server("Move", {
	T.Instance(),
	T.Vector3(),
	T.UInt8(),
})
```

## How a schema is applied

A schema is a positional array of type constructors. Entry `n` encodes argument `n`.

<Note>
  A schema is a positional list. `schema[1]` encodes the first argument, `schema[2]`
  the second, and so on. The number of arguments you pass to `Fire` has to match the
  number of entries in the schema. Passing more arguments than the schema describes
  will error when Flux reaches a `nil` entry, and passing fewer writes a shorter
  packet than the receiver expects to read.
</Note>

<Warning>
  Schemas apply to events only. A schema encodes the payloads of `Fire`,
  `FireUnreliable`, `FireAll` and `FireAllUnreliable`. `Invoke` requests and their
  responses always fall back to the dynamic `WriteAny`/`ReadAny` encoder, whatever
  schema you passed to `Flux.Server` or `Flux.Client`.
</Warning>

<Warning>
  Both sides have to declare the same schema. Flux does not negotiate or validate
  layouts. The sender writes what its schema says and the receiver reads what its schema
  says. A mismatch does not raise a clear error; it silently misreads the buffer and
  produces garbage or a cursor overrun.
</Warning>

Because schemas are registered globally by event name, the cleanest way to keep the two
sides in sync is to define the schema once in a shared module:

<CodeGroup>
  ```luau ReplicatedStorage/Net.luau theme={null}
  local Flux = require(game:GetService("ReplicatedStorage").Flux)
  local T = Flux.Types

  return {
  	Move = {
  		T.Instance(),
  		T.Vector3(),
  		T.UInt8(),
  	},

  	Damage = {
  		T.Instance(),
  		T.UInt16(),
  	},
  }
  ```

  ```luau Server theme={null}
  local Schemas = require(ReplicatedStorage.Net)

  local Move = Flux.Server("Move", Schemas.Move)
  Move:FireAllUnreliable(character, position, 3)
  ```

  ```luau Client theme={null}
  local Schemas = require(ReplicatedStorage.Net)

  local Move = Flux.Client("Move", Schemas.Move)
  Move:Connect(function(character, position, state)
  	-- ...
  end)
  ```
</CodeGroup>

## What you save

<Tabs>
  <Tab title="Without a schema">
    ```luau theme={null}
    local Damage = Flux.Server("Damage")
    Damage:FireAll(humanoid, 250)
    ```

    ```
    tag(1) + instIndex(1)   Instance
    tag(1) + u16(2)         250, narrowed to u16 by the dynamic encoder
    =========================
    5 bytes of payload
    ```
  </Tab>

  <Tab title="With a schema">
    ```luau theme={null}
    local Damage = Flux.Server("Damage", { T.Instance(), T.UInt16() })
    Damage:FireAll(humanoid, 250)
    ```

    ```
    instIndex(1)            Instance
    u16(2)                  250
    =========================
    3 bytes of payload
    ```
  </Tab>
</Tabs>

<Info>
  The dynamic encoder already narrows integers to the smallest width that fits, so a
  schema's saving on small integers is mostly the tag byte per value. The bigger wins
  are elsewhere: forcing a `Float32` where the encoder would have written an 8-byte
  `f64`, and `T.Enum()`, which sends a one-byte index instead of the full
  `"Enum.KeyCode.Space"` string.
</Info>

### When a schema is worth it

<AccordionGroup>
  <Accordion title="High-frequency unreliable streams" icon="wave-square">
    Replicating positions every frame is where per-packet bytes actually add up, and it
    is also where `UnreliableRemoteEvent`'s size limit bites. A schema with
    `T.Float32()` components is the single most effective change you can make.
  </Accordion>

  <Accordion title="Floats you do not need at full precision" icon="ruler-horizontal">
    The dynamic encoder writes every non-integer as an `f64`. `T.Float32()` halves that,
    and for world positions or normalised values the precision loss is irrelevant.
  </Accordion>

  <Accordion title="Enums" icon="list">
    Dynamically encoded `EnumItem`s are sent as their full string name. `T.Enum()` sends
    a single byte. If you send enums at any volume, schematise them.
  </Accordion>

  <Accordion title="Not worth it for low-traffic events" icon="circle-minus">
    A purchase confirmation fired twice a minute does not need a schema. The dynamic
    encoder is convenient, tolerates changing argument shapes, and costs you nothing
    measurable at that rate.
  </Accordion>
</AccordionGroup>

## Composites

Schema entries nest, so you can describe structured payloads without falling back to
the dynamic encoder.

### Optional

Prefixes the value with a single boolean byte. This is the only correct way to send a
value that may be `nil` under a schema.

```luau theme={null}
local Target = Flux.Server("Target", {
	T.Optional(T.Instance()),
})

Target:Fire(player, currentTarget) -- currentTarget may be nil
```

<Warning>
  A plain `T.Instance()` or `T.UInt8()` cannot encode `nil`. Passing `nil` to a
  non-optional entry will error inside `buffer.write*`. Wrap anything nullable in
  `T.Optional`.
</Warning>

### Array

Writes a varint length followed by each element.

```luau theme={null}
local Leaderboard = Flux.Server("Leaderboard", {
	T.Array(T.String(20)),  -- names
	T.Array(T.UInt32(), 50) -- scores, at most 50 entries
})

Leaderboard:FireAll(names, scores)
```

### Struct

Encodes a dictionary with named fields in a declared order. Entries are
`{ name, type }` pairs.

```luau theme={null}
local Spawn = Flux.Server("Spawn", {
	T.Struct({
		{ "Position", T.Vector3() },
		{ "Health", T.UInt16() },
		{ "Team", T.Enum(Enum.TeamColor) },
	}),
})

Spawn:FireAll({
	Position = Vector3.new(0, 10, 0),
	Health = 100,
	Team = Enum.TeamColor.Bright_blue,
})
```

<Note>
  Only the declared fields are transmitted. Extra keys on the table are ignored, and a
  missing key is written as `nil`, which will error for a non-optional field type. Field
  order is part of the wire format, so both sides have to declare the fields in the same
  sequence.
</Note>

### Nesting

Composites compose freely:

```luau theme={null}
local Squad = Flux.Server("Squad", {
	T.Array(T.Struct({
		{ "Player", T.Instance() },
		{ "Loadout", T.Array(T.String(24), 8) },
		{ "Captain", T.Optional(T.Bool()) },
	})),
})
```

## Escaping a schema

`T.Any()` uses the dynamic encoder for that one entry. Use it when a single argument's
shape genuinely varies but the rest of the payload is fixed.

```luau theme={null}
local Analytics = Flux.Server("Analytics", {
	T.String(64),  -- event name, always a string
	T.Any(),       -- arbitrary payload table
})
```

## Bounded strings and arrays

`T.String(maxLen)` and `T.Array(t, maxLen)` assert against the limit.

```luau theme={null}
local Chat = Flux.Server("Chat", { T.String(200) })
```

<Warning>
  The bound is checked on write only, and it raises a Luau error rather than dropping
  the message. On the server, a `Fire` with an over-long string will throw in your own
  code; on the client, an over-long `Fire` throws before anything is sent. Never treat
  `maxLen` as inbound validation of untrusted client data. Always re-check length inside
  your server listener.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="Schema constructors" icon="list" href="/api/types">
    Every entry in the `Flux.Types` namespace.
  </Card>

  <Card title="Serialization" icon="binary" href="/concepts/serialization">
    How the dynamic encoder decides what to write.
  </Card>
</CardGroup>
