START HERE·PRACTICE TEMPLATES·Verified June 2026 · Lua 5.4 · ox_lib 3.x
Learning with an AI assistant?
Copies this whole lesson - every step, code block, and the exact console errors - plus 2026 ground rules (no lua54 'yes', Cfx.re Portal, correct callback signatures) as a ready-to-paste mentor prompt.
Start Here · Practice Templates

Teleport command (intermediate)

This is not a tutorial you read. It is a half-finished resource you finish. The scaffold below boots and registers a /tp command, but the parts that make it safe are missing on purpose. You write the location lookup, the cooldown, and the teleport itself, then reveal a working solution to check your work against. By the end you will understand why a real server keeps its rules on the server and lets the client do only what the client is allowed to do.

Goal
A /tp command that teleports a player to a saved, named location, with a cooldown and a server-side permission check.
Difficulty
Intermediate
Estimated time
~25 minutes
Skills practiced
commands, tables, client/server split, server authority, cooldowns

Watch

Quasar: the teleport script walkthrough.
BEFORE YOU START

The goal

You are building a resource named qu_tp. It gives staff a /tp <name> command that drops a player at a saved spot, like /tp bank or /tp garage. The spots live in a Config.Locations table so you can add more without touching the logic.

Three things have to be true for this to be a real feature and not a toy:

  • A player who types /tp bank and is allowed to use the command arrives at the bank.
  • A player who just teleported and types /tp again right away is told to wait, because there is a cooldown.
  • The decision about who may teleport and how often is made on the server, never on the player's machine.

That last point is the whole lesson. The teleport itself has to happen on the client, because moving a ped is a client action. But the permission and the cooldown are rules, and rules that a player could edit are not rules. So the command lives on the server, the server decides yes or no, and only then does it ask the client to move the ped.

Vocabulary

Server authority
The principle that the server, not the player, decides what is allowed. The client can ask, the server decides. Anything a player could cheat by editing must be checked server-side.
Cooldown
A minimum wait between uses of an action, tracked per player so one person spamming a command cannot abuse it.
source
The number FiveM gives a server command handler to identify which player ran it. Player 3 is source 3. The console is source 0.

What you start with

Create a resource folder qu_tp with three files. Paste each block exactly. This scaffold runs as-is: the command is wired and the client event exists. What is missing are the three checks and the one native call that make it work, each marked with a -- TODO: comment.

fxmanifest.lua

lua
fx_version 'cerulean'
game 'gta5'
 
shared_script 'config.lua'
server_script 'server.lua'
client_script 'client.lua'

config.lua

lua
Config = {}
 
-- Named teleport destinations. x, y, z are world coordinates.
Config.Locations = {
    bank   = { x = 150.0,  y = -1040.0, z = 29.4 },
    garage = { x = -337.0, y = -136.0,  z = 39.0 },
    pier   = { x = -1850.0, y = -1230.0, z = 13.0 },
}
 
-- How long a player must wait between teleports, in seconds.
Config.Cooldown = 10

server.lua

lua
-- Tracks the last time each player teleported, keyed by their source.
local cooldowns = {}
 
RegisterCommand('tp', function(source, args)
    local locationName = args[1]
 
    -- Look up the requested location in Config.Locations.
    local target = Config.Locations[locationName]
 
    -- TODO: if target is nil, the location name was unknown.
    -- Tell the player and return early so nothing else runs.
 
    -- TODO: cooldown check.
    -- Read cooldowns[source]. If the player teleported less than
    -- Config.Cooldown seconds ago, tell them how long to wait and return.
 
    -- TODO: record this teleport time in cooldowns[source] using os.time().
 
    -- All checks passed. Tell the client to teleport.
    TriggerClientEvent('qu_tp:teleport', source, target.x, target.y, target.z)
end, true)

client.lua

lua
RegisterNetEvent('qu_tp:teleport', function(x, y, z)
    local ped = PlayerPedId()
 
    -- TODO: move the player's ped to x, y, z using the SetEntityCoords native.
end)

If you ensure this resource now and run /tp bank from the txAdmin Live Console, the command fires and the client event triggers, but the ped does not move, because the one native that moves it is still a TODO. That is expected. Your job is to fill the gaps.

Your job

Checklist

  • Reject unknown names. If args[1] is not a key in Config.Locations, target is nil. Stop right there: tell the player the location is unknown and return so the rest of the handler never runs.
  • Enforce a per-player cooldown. Use the cooldowns table keyed by source plus os.time(). If the player used /tp less than Config.Cooldown seconds ago, tell them how many seconds are left and return.
  • Record the teleport time. Once the checks pass, write os.time() into cooldowns[source] so the next call can measure the gap.
  • Keep both checks on the server. The permission flag and the cooldown logic stay in server.lua. Never move them to the client. A player can edit client files; they cannot edit yours.
  • Do the teleport on the client. In client.lua, set the ped's coordinates to the x, y, z the server sent, using SetEntityCoords.

How you tell the player is up to you. The simplest path is a print on the server (it lands in the console) while you are testing. A nicer version sends chat back to the player. The reveal uses a server-side notify event so the message reaches the actual player who ran the command, not just the console.

Hints

Hints for each TODO
  • Cooldown table. cooldowns is a plain Lua table. Key it by source so each player has their own entry: cooldowns[source]. The value is a timestamp from os.time(), which returns the current time in whole seconds. To find how long ago a player teleported, subtract: os.time() - cooldowns[source]. The first time a player runs the command, cooldowns[source] is nil, so guard for that before you subtract.
  • Reject early. A missing or wrong name means target is nil. Check if not target then near the top, message the player, and return. Returning early keeps the rest of the handler from running on bad input, which is cleaner than wrapping everything in a giant if.
  • Order matters. Check the name, then the cooldown, then record the time, then trigger the client. If you record the time before the cooldown check, you lock the player out on their very first try.
  • The teleport. On the client, SetEntityCoords(PlayerPedId(), x, y, z, false, false, false, true) moves the player's character to the coordinates. The trailing booleans are FiveM flags; the last true clears the area so the player does not land inside another object.
Why can the cooldown check not just live in client.lua, where the teleport already happens?

Because a player controls their own client. Any rule you write in client.lua is running on the player's machine, and a cheater can edit or delete it. If the cooldown lived on the client, someone could strip it out and teleport every frame. The server is the one machine the player cannot touch, so every rule that is meant to constrain the player, permission and cooldown included, has to be enforced there. The client's only job is to do the harmless visible part: move the ped to coordinates the server already approved.

Reveal the solution

Stuck or done? Reveal a working solution

Here are the three finished files. Compare them to yours. The shape matters more than matching every word: your messages and your exact booleans can differ, as long as the name check, the cooldown, and the client/server split are all in the right place.

config.lua (unchanged from the scaffold, shown for completeness):

lua
Config = {}
 
-- Named teleport destinations. x, y, z are world coordinates.
Config.Locations = {
    bank   = { x = 150.0,  y = -1040.0, z = 29.4 },
    garage = { x = -337.0, y = -136.0,  z = 39.0 },
    pier   = { x = -1850.0, y = -1230.0, z = 13.0 },
}
 
-- How long a player must wait between teleports, in seconds.
Config.Cooldown = 10

server.lua:

lua
-- Tracks the last time each player teleported, keyed by their source.
local cooldowns = {}
 
local function notify(source, message)
    TriggerClientEvent('qu_tp:notify', source, message)
end
 
RegisterCommand('tp', function(source, args)
    local locationName = args[1]
    local target = Config.Locations[locationName]
 
    -- Reject unknown names.
    if not target then
        notify(source, 'Unknown location. Try: bank, garage, pier.')
        return
    end
 
    -- Cooldown check. lastUse is nil on the first ever use.
    local now = os.time()
    local lastUse = cooldowns[source]
    if lastUse and (now - lastUse) < Config.Cooldown then
        local remaining = Config.Cooldown - (now - lastUse)
        notify(source, ('Wait %d seconds before teleporting again.'):format(remaining))
        return
    end
 
    -- Record this teleport so the next call can measure the gap.
    cooldowns[source] = now
 
    -- All checks passed. Tell the client to move the ped.
    TriggerClientEvent('qu_tp:teleport', source, target.x, target.y, target.z)
    notify(source, ('Teleported to %s.'):format(locationName))
end, true)

client.lua:

lua
RegisterNetEvent('qu_tp:teleport', function(x, y, z)
    local ped = PlayerPedId()
    SetEntityCoords(ped, x, y, z, false, false, false, true)
end)
 
RegisterNetEvent('qu_tp:notify', function(message)
    print('[qu_tp] ' .. message)
end)

Why the cooldown and permission live on the server. The command is registered with the restricted flag (true), so FiveM only lets the server console and ACE-permitted players run it. That permission gate is enforced by the server before your handler even runs. The cooldown is enforced by your own code, also on the server, in the cooldowns table that no player can reach. The client never decides anything. It receives coordinates the server already approved and calls SetEntityCoords, which is a client native and so can only run there. The split is the point: rules on the server, the visible action on the client. A player can edit client.lua all day and never gain a faster teleport or access they were not granted, because the gate they would need to break is on a machine they do not control.

To test it, ensure qu_tp, then from the txAdmin Live Console run restart qu_tp and tp bank. Run tp bank again immediately and you should see the wait message. Run tp moon and you should see the unknown-location message.

What you practiced

  • Look up a value in a config table by key and reject input that is not in the table by returning early.
  • Track per-player state in a Lua table keyed by source, and use os.time() to measure how long ago something happened.
  • Enforce a cooldown so a single player cannot spam a command.
  • Keep permission and cooldown on the server, where the player cannot edit them, and send only approved data to the client.
  • Call a client native (SetEntityCoords) on the client after the server has signed off, completing a real client/server round trip.

When this clicks, the natural next step is the advanced template, "Shop with server validation (advanced)," where the server checks a player's money and inventory before letting a purchase go through. It is the same lesson with higher stakes: never trust the client with anything it could lie about.