# Nord Vehicle Keys

Official Nord Lab documentation for Nord Vehicle Keys. Covers installation, strict physical keys, vehicle locking, theft systems, stolen-key expiry, recovery NPCs, ghost placement, bridges, developer exports, database, security and troubleshooting.

# 1 · Getting Started

Install Nord Vehicle Keys, understand its core concepts and verify a clean first boot.

# Overview

Nord Vehicle Keys is a server-authoritative vehicle access system for FiveM. It combines physical vehicle keys, vehicle locking, theft gameplay, temporary stolen keys, recovery NPCs and a modular bridge layer for frameworks, inventories and UI providers.

## Main features

- Physical `vehicle_key` items with plate metadata.
- Server-authoritative lock state using entity state bags.
- Lock / unlock from keybind, commands or the physical-key remote.
- Permanent owner keys, shared keys, temporary keys and stolen/session keys.
- Carjacking of NPC drivers by aiming a valid firearm.
- Lockpick and hotwire flows with configurable difficulty and alarm chances.
- Stolen-key expiry when leaving the stolen vehicle plus a safety timeout.
- Persistent key-recovery NPCs for owned vehicles.
- In-game admin panel for recovery NPC management.
- Ghost NPC placement editor with movement, rotation, height and ground snapping.
- Framework, inventory, notification and Text UI bridges isolated from the core.
- Server exports for garages, dealerships, rentals and other trusted resources.

## Design principle

Normal access is intentionally strict when physical keys are enabled:

> If the player no longer has the matching physical key item, the resource does not silently recreate it during an access check.

A missing owner key must be deliberately reissued through the recovery NPC, an admin action or a trusted server export.


# Requirements & Installation

## Requirements

| Requirement | Status |
|---|---|
| FiveM server | Required |
| OneSync | Required for authoritative entity checks |
| `oxmysql` | Required when persistence/ownership tables are used |
| QBCore / QBX / ESX | Optional |
| `nord_inventory` | Recommended for native physical-key metadata |
| `ox_inventory` | Supported for generic item checks |
| `ox_lib` | Optional notification/Text UI provider |

## Resource installation

1. Copy the folder as `nord_vehiclekeys` into your resources directory.
2. Start the database resource first.
3. Start your framework and inventory before Nord Vehicle Keys.
4. Add the recovery admin ACE if staff will manage recovery NPCs.
5. Ensure the resource.

```cfg
ensure oxmysql
ensure qb-core          # or qbx_core / es_extended when used
ensure nord_inventory   # recommended for physical vehicle_key items
ensure nord_vehiclekeys
```

Recovery admin permission:

```cfg
add_ace group.admin nord.vehiclekeys.admin allow
```

## Database

Auto SQL is enabled by default. The resource creates and validates its own tables at startup.

For a manual install, import:

```text
sql/install.sql
```


# First Boot Checklist

After installation, verify these points before opening the server to players.

1. `oxmysql` is started before Nord Vehicle Keys.
2. The selected framework bridge is detected correctly.
3. The selected inventory bridge is available.
4. OneSync is enabled.
5. No SQL or Lua errors appear during resource startup.
6. The `vehicle_key` item is registered when Nord Inventory auto-registration is enabled.
7. `/vehiclekeysadmin` opens for an ACE-authorized admin.
8. Lock/unlock works on an owned test vehicle.
9. Removing the physical owner key removes normal access when strict physical-key mode is enabled.
10. Recovering that key through a recovery NPC reissues access correctly.

## Recommended start order

```cfg
ensure oxmysql
ensure your_framework
ensure nord_inventory
ensure nord_vehiclekeys
```

If you use another inventory, replace `nord_inventory` with that resource and configure the Inventory bridge accordingly.


# Resource Structure

The resource is intentionally split so integrations can be changed without editing the core logic.

```text
nord_vehiclekeys/
├─ config/
│  ├─ bridges.lua
│  └─ main.lua
├─ client/
│  ├─ bridge/
│  │  ├─ framework.lua
│  │  ├─ inventory.lua
│  │  ├─ notify.lua
│  │  ├─ textui.lua
│  │  └─ minigame.lua
│  ├─ main.lua
│  ├─ theft.lua
│  ├─ stolen.lua
│  ├─ placement.lua
│  └─ recovery.lua
├─ server/
│  ├─ bridge/
│  │  ├─ framework.lua
│  │  ├─ inventory.lua
│  │  ├─ notify.lua
│  │  ├─ textui.lua
│  │  ├─ ownership.lua
│  │  └─ nord_inventory_registration.lua
│  ├─ api.lua
│  ├─ main.lua
│  ├─ stolen.lua
│  ├─ recovery.lua
│  └─ storage.lua
├─ shared/
├─ locales/
├─ web/
└─ sql/
```

For normal compatibility work, edit the appropriate file under `client/bridge/` or `server/bridge/` instead of changing `main.lua`.

# 2 · Configuration

Configure key behaviour, vehicle state, sharing, recovery, theft systems and persistence.

# Core Configuration

The primary configuration file is:

```text
config/main.lua
```

Common options:

```lua
Config.Debug = false
Config.Locale = 'en'

Config.Keybinds = {
    Lock = 'L',
    Hotwire = 'H'
}
```

Distances are controlled separately:

```lua
Config.Distances = {
    ToggleLock = 18.0,
    Action = 7.0,
    Share = 7.0,
    Ensure = 35.0
}
```

## Vehicle state

```lua
Config.VehicleState = {
    OwnedVehiclesSpawnLocked = true,
    NPCVehiclesSpawnLockedChance = 65,
    NPCLocksPlayerOnly = true,
    NPCKeysInsideChance = 15,
}
```

`NPCLocksPlayerOnly` keeps ambient NPC traffic usable by GTA AI while still enforcing the logical player-facing lock state.


# Physical Keys

Physical-key behaviour is configured under `Config.PhysicalKeys`.

```lua
Config.PhysicalKeys = {
    Enabled = true,
    Item = 'vehicle_key',
    SyncWithAccess = true,
    ItemGrantsAccess = true,
    RequirePhysicalItemForAccess = true,
    OwnerAccessWithoutPhysicalItem = false,
    StoredAccessWithoutPhysicalItem = false,
    AllowClientOwnerClaim = false,
    IssueForJobAccess = false,
}
```

## Strict mode behaviour

With the default strict settings:

- A physical key is authoritative for normal owner/shared access.
- Deleting or losing the item removes that physical access.
- Ownership alone does not silently recreate a missing key.
- A client cannot claim an owner key simply because it is near an owned vehicle.
- Trusted server resources should intentionally call `GiveKeys` when issuing a new key.

This prevents accidental unlimited key regeneration.


# Remote Configuration

Using a physical `vehicle_key` can open the built-in vehicle remote.

```lua
Config.PhysicalKeys.Remote = {
    Enabled = true,
    Range = 25.0,
    RefreshMs = 250,
    AllowTrunk = true,
    CloseOnAction = false,
    TrunkDoors = {},
}
```

The remote supports:

- Lock
- Unlock
- Open/close supported trunk
- Live in-range status
- Plate display

The remote closes with `ESC`. Its NUI is transparent while closed and does not create a fullscreen dark overlay.

`TrunkDoors` can be used to explicitly map unusual/front-trunk vehicles to a door index.


# Sharing & Job Access

## Key sharing

```lua
Config.ShareKeys = {
    Enabled = true,
    AllowReshare = false,
    DefaultDurationSeconds = 3600,
    PersistTemporaryKeys = true,
}
```

Player commands:

```text
/givekey [server id]
/revokekey [server id]
```

The player must use the current vehicle or a nearby vehicle within the configured share distance.

## Job access

Job access is empty by default to avoid unintended fleet access.

Example:

```lua
Config.JobAccess.police = {
    minGrade = 0,
    requireDuty = true,
    platePrefixes = { 'LSPD', 'POL' },
    models = { 'police', 'police2', 'police3' }
}
```

Job access can stay virtual so shared fleet vehicles do not generate large numbers of physical key items.


# Persistence & Ownership

## Persistence

```lua
Config.Persistence = {
    Enabled = true,
    Resource = 'oxmysql',
    AutoInstall = true,
    QueryTimeoutMs = 10000,
    StartupTimeoutMs = 60000,
    RetryIntervalMs = 1000,
    PersistPermanentSharedKeys = true,
    PersistTemporaryKeys = true,
}
```

## Ownership tables

Default ownership mappings:

```lua
Config.Ownership.QB = {
    table = 'player_vehicles',
    ownerColumn = 'citizenid',
    plateColumn = 'plate',
    vehicleColumn = 'vehicle',
}

Config.Ownership.ESX = {
    table = 'owned_vehicles',
    ownerColumn = 'owner',
    plateColumn = 'plate',
    vehicleColumn = 'vehicle',
}
```

QBX uses the same default `player_vehicles` layout as QB.

For a custom garage schema, either change the configured table/columns or enable custom ownership and implement the ownership bridge.

# 3 · Vehicle Keys & Locking

Understand key types, lock state and the built-in player commands.

# Key Types

Nord Vehicle Keys uses different key types for different access lifecycles.

| Key type | Typical use | Persistence |
|---|---|---|
| `owner` | Player-owned vehicle | Permanent |
| `shared` | Shared access | Permanent or timed |
| `temporary` | Rentals/scripts | Timed |
| `session` | Stolen/NPC vehicle | Temporary session |
| `admin` | Administrative grant | Permanent until removed |

When a physical key item is issued, metadata includes at least:

```lua
{
    plate = 'ABC123',
    keyType = 'owner',
    expiresAt = 0,
    sessionToken = ''
}
```

Session keys use a session token that is validated server-side and cannot be reused by another player connection.


# Locking Controls

Default lock controls:

```text
L                  Toggle current/nearest vehicle lock
/vehiclelock       Toggle current/nearest vehicle lock
/lockvehicle       Lock current/nearest vehicle
/unlockvehicle     Unlock current/nearest vehicle
```

The lock key is configurable through `Config.Keybinds.Lock` and FiveM key mappings.

Lock state is synchronized using server-controlled vehicle state bags. This allows all clients to apply the same logical lock state while the server remains authoritative for access validation.


# Admin Key Commands

Administrative key commands require the ACE configured in `Config.Security.AdminAce`.

Default ACE:

```cfg
add_ace group.admin nord.vehiclekeys.admin allow
```

Commands:

```text
/adminkey [server id] [plate]
/adminkeyremove [server id] [plate]
```

`/adminkey` issues administrative access and synchronizes the matching physical key item when physical keys are active.

`/adminkeyremove` removes stored access and the matching physical key item.

# 4 · Theft & Stolen Keys

Configure and operate carjacking, lockpick, hotwire and stolen-key cleanup.

# Carjacking

Carjacking can start automatically when a player holds aim on an NPC driver with an allowed firearm.

```lua
Config.Carjacking = {
    Enabled = true,
    AimHoldMs = 850,
    DurationMs = 3200,
    CooldownMs = 10000,
    GiveKeysOnSuccess = true,
    SuccessChance = 100,
    AlarmChance = 35,
    RequireWeapon = true,
}
```

The NPC/vehicle action is reserved and validated server-side. The finish step validates the reserved vehicle, plate, action token, timing and distance without depending on volatile NPC network ownership.

Blocked melee, utility and throwable weapons can be configured through `Config.Carjacking.BlockedWeapons`.


# Lockpick

Lockpick configuration:

```lua
Config.Lockpick = {
    Enabled = true,
    Item = 'lockpick',
    DurationMs = 6500,
    Difficulty = 4,
    BreakChanceOnFail = 35,
    AlarmChanceOnFail = 70,
    AlarmChanceOnSuccess = 15,
    GiveKeysWhenKeysFoundInside = true,
}
```

The configured inventory bridge validates and removes the item when required.

Command:

```text
/lockpick
```

Developer client export:

```lua
exports.nord_vehiclekeys:StartLockpick(vehicle)
```


# Hotwire

Hotwire configuration:

```lua
Config.Hotwire = {
    Enabled = true,
    Item = nil,
    DurationMs = 9000,
    Difficulty = 6,
    FailCooldownMs = 3500,
    AlarmChanceOnFail = 65,
    AlarmChanceOnSuccess = 20,
}
```

Set `Item` to an item name if a hotwire kit should be required. When `nil`, no item is required.

Command:

```text
/nordkeys_hotwire
```

Developer client export:

```lua
exports.nord_vehiclekeys:StartHotwire(vehicle)
```


# Stolen Key Lifecycle

Keys obtained through NPC theft, lockpick-found-keys or hotwire are temporary stolen/session keys.

```lua
Config.StolenKeys = {
    RemoveOnVehicleExit = true,
    MaxDurationSeconds = 900,
    ExitGraceMs = 1200,
    ServerCheckMs = 750,
}
```

Default behaviour:

1. The stolen key is issued for the specific vehicle.
2. The player can use the vehicle normally while the stolen key is valid.
3. After the player enters the vehicle and later exits it, the stolen key is revoked.
4. The physical `vehicle_key` item for that stolen access is removed.
5. A safety timeout removes abandoned session keys even if the player never enters the vehicle.

Permanent owner/shared/admin keys are not removed by the stolen-key cleanup flow.


# Blocked Vehicle Classes

The theft system can block GTA vehicle classes and models.

Default blocked classes:

```lua
Config.Theft.BlockedClasses = {
    [14] = true, -- boats
    [15] = true, -- helicopters
    [16] = true, -- planes
    [18] = true, -- emergency
    [19] = true, -- military
    [21] = true, -- trains
}
```

Specific models can also be blocked:

```lua
Config.Theft.BlockedModels = {
    'rhino',
}
```

# 5 · Recovery NPC & Placement

Manage persistent recovery NPCs and use the ghost placement editor.

# Recovery Service

The recovery NPC allows players to reissue a lost physical owner key for a vehicle the server confirms they own.

It does **not** grant keys for arbitrary plates.

Recovery flow:

1. Player approaches a configured recovery NPC.
2. The NPC UI requests the player's owned vehicles from the server.
3. Ownership is read from the configured framework vehicle table/custom ownership bridge.
4. Player selects a vehicle.
5. Ownership is validated again server-side.
6. A permanent owner key and physical `vehicle_key` item are issued.

Configuration:

```lua
Config.Recovery = {
    Enabled = true,
    AdminCommand = 'vehiclekeysadmin',
    AdminAce = 'nord.vehiclekeys.admin',
    DefaultModel = 's_m_m_autoshop_01',
    DefaultLabel = 'Vehicle Key Service',
    DefaultScenario = 'WORLD_HUMAN_CLIPBOARD',
    InteractDistance = 2.2,
    StreamDistance = 90.0,
    Fee = 0,
}
```


# Recovery Admin Panel

Open the recovery NPC admin panel with:

```text
/vehiclekeysadmin
```

Required ACE:

```cfg
add_ace group.admin nord.vehiclekeys.admin allow
```

The panel is in-game only and can be used to:

- Create recovery NPCs.
- Edit label/model/scenario.
- Move an existing NPC.
- Enter the visual placement editor.
- Delete recovery NPCs.

Recovery NPCs are persisted in the database and respawn after resource/server restarts.


# Ghost Placement Editor

Creating or editing a recovery NPC can open the visual placement editor.

The preview NPC is transparent, non-colliding and frozen while being positioned.

## Controls

| Control | Action |
|---|---|
| `W A S D` | Move relative to camera |
| Left mouse | Drag ghost toward the point under the cursor/raycast |
| Arrow Up / Down | Raise / lower |
| Arrow Left / Right | Rotate |
| `Q / E` | Rotate |
| `SHIFT` | Fast movement/rotation |
| `CTRL` | Precision movement |
| `G` | Snap to ground |
| `F` | Move to raycast point |
| `R` | Face camera direction |
| `ENTER` | Confirm and save |
| `ESC` / `BACKSPACE` | Cancel |

Placement configuration:

```lua
Config.Recovery.Placement = {
    Enabled = true,
    GhostAlpha = 145,
    StartDistance = 2.0,
    MoveStep = 0.035,
    VerticalStep = 0.025,
    RotateStep = 1.0,
    FastMultiplier = 4.0,
    PrecisionMultiplier = 0.25,
    RaycastDistance = 25.0,
    MaxDistanceFromAdmin = 35.0,
    SnapToGroundOnStart = true,
    DrawMarker = true,
}
```

The saved X/Y/Z/heading from the ghost is authoritative for the final NPC. The final spawn does not perform a second destructive ground snap.

# 6 · Bridges & Compatibility

Keep custom integrations isolated from the core by using the bridge layer.

# Bridge Architecture

Bridge selection lives in:

```text
config/bridges.lua
```

```lua
Config.Bridges = {
    Framework = 'auto', -- auto | qb | qbx | esx | standalone | custom
    Inventory = 'auto', -- auto | nord | ox | qb | esx | custom
    Notify = 'auto',    -- auto | ox | qb | esx | gta | custom
    TextUI = 'auto',    -- auto | ox | gta | custom
}
```

For a custom resource:

1. Set the relevant bridge to `custom`.
2. Edit only the matching bridge file.
3. Return the values expected by the bridge contract.
4. Leave the core resource logic unchanged.

This keeps updates easier because custom integrations stay isolated.


# Framework Bridge

Framework bridge files:

```text
client/bridge/framework.lua
server/bridge/framework.lua
```

Auto-detection order:

1. `qbx_core`
2. `qb-core`
3. `es_extended`
4. standalone fallback

The server bridge provides:

- Framework detection.
- Player lookup.
- Stable identifier lookup.
- Job lookup.
- Grade normalization.
- Duty state handling.

Custom integration hooks:

```lua
NVKServerBridge.CustomGetPlayer(source)
NVKServerBridge.CustomGetIdentifier(source)
NVKServerBridge.CustomGetJob(source)
NVKClientBridge.CustomGetJob()
```

Return your framework's normal player/job objects in those hooks.


# Inventory Bridge

Inventory bridge files:

```text
client/bridge/inventory.lua
server/bridge/inventory.lua
```

Generic item checks support:

- Nord Inventory
- ox_inventory
- QB/QBX inventory through the framework player object
- ESX inventory
- Custom implementation

## Native physical vehicle keys

The full physical `vehicle_key` metadata flow is native to `nord_inventory`.

Custom inventory hooks are provided for complete physical-key support:

```lua
NVKInventory.CustomHasItem(source, item, count)
NVKInventory.CustomRemoveItem(source, item, count)
NVKInventory.CustomHasVehicleKeyItem(source, plate)
NVKInventory.CustomResolveVehicleKeyMetadata(source, slot)
NVKInventory.CustomGiveVehicleKeyItem(source, plate, keyType, expiresAt, sessionBound)
NVKInventory.CustomRemoveVehicleKeyItem(source, plate)
NVKInventory.CustomCleanupInvalidVehicleKeyItems(source)
```

Client custom count hook:

```lua
NVKClientInventory.CustomGetItemCount(item, metadata)
```

If you use a custom inventory with strict physical keys, implement the physical-key hooks rather than modifying the core.


# Notifications & Text UI

Notification and Text UI are independent bridges.

Files:

```text
client/bridge/notify.lua
server/bridge/notify.lua
client/bridge/textui.lua
server/bridge/textui.lua
```

Supported notification modes:

```text
auto | ox | qb | esx | gta | custom
```

Supported Text UI modes:

```text
auto | ox | gta | custom
```

Custom notification hook:

```lua
function NVKNotify.Custom(message, kind, duration, title)
    -- show your notification
    return true
end
```

Custom Text UI hooks:

```lua
function NVKTextUI.CustomShow(id, text, options)
    -- show your UI
    return true
end

function NVKTextUI.CustomHide(id)
    -- hide your UI
    return true
end
```

The recovery interaction uses this bridge instead of hardcoded GTA help text.


# Custom Ownership Bridge

Ownership is isolated in:

```text
server/bridge/ownership.lua
```

For custom ownership logic:

```lua
Config.Ownership.Custom = true
```

Implement:

```lua
function NVKOwnership.CustomGetOwnerIdentifier(plate)
    -- Return the framework identifier that owns this plate.
end
```

For the recovery UI, you can also implement:

```lua
function NVKOwnership.CustomGetOwnedVehicles(source, identifier)
    return {
        { plate = 'ABC123', model = 'sultan' },
        { plate = 'XYZ987', model = 'adder' },
    }
end
```

Trusted standalone/garage resources can set a runtime owner through the `SetVehicleOwner` server export.

# 7 · Developer API

Integrate garages, dealerships, rentals and other server resources through stable exports.

# Server Key Exports

Key creation and removal should be performed from trusted **server-side** resources.

## Recommended simple API

```lua
-- Permanent owner key + physical key item.
exports.nord_vehiclekeys:GiveKeys(source, plate)

-- Remove access/key for the plate.
exports.nord_vehiclekeys:RemoveKeys(source, plate)

-- Check access.
local hasKey, keyType, expiresAt = exports.nord_vehiclekeys:HasKeys(source, plate)
```

Aliases:

```lua
exports.nord_vehiclekeys:AddKey(source, plate)
exports.nord_vehiclekeys:DeleteKey(source, plate)
exports.nord_vehiclekeys:GiveOwnerKey(source, plate)
```

Shared and temporary keys:

```lua
exports.nord_vehiclekeys:GiveSharedKey(source, plate, 0)      -- permanent shared
exports.nord_vehiclekeys:GiveSharedKey(source, plate, 3600)   -- timed shared
exports.nord_vehiclekeys:GiveTempKey(source, plate, 900)      -- temporary
```

Advanced API:

```lua
exports.nord_vehiclekeys:GiveKey(source, plate, 'owner', 0)
exports.nord_vehiclekeys:GiveKey(source, plate, 'shared', 3600)
exports.nord_vehiclekeys:GiveTemporaryKey(source, plate, 900)
exports.nord_vehiclekeys:RemoveKey(source, plate)
exports.nord_vehiclekeys:HasKey(source, plate)
```


# Physical Key Item Exports

These exports work directly with the physical key item layer.

```lua
exports.nord_vehiclekeys:GivePhysicalKeyItem(source, plate, 'owner', 0)
exports.nord_vehiclekeys:RemovePhysicalKeyItem(source, plate)
exports.nord_vehiclekeys:HasPhysicalKeyItem(source, plate)
```

Normally, use `GiveKeys` / `RemoveKeys` instead because they keep permission state and the physical item synchronized.

Use the physical-item exports only when you deliberately need low-level control.


# Server Vehicle Exports

Vehicle state exports operate on a network ID.

```lua
exports.nord_vehiclekeys:SetLockState(netId, true)
exports.nord_vehiclekeys:GetLockState(netId)
exports.nord_vehiclekeys:ToggleLock(netId)
exports.nord_vehiclekeys:LockVehicle(netId)
exports.nord_vehiclekeys:UnlockVehicle(netId)
exports.nord_vehiclekeys:RegisterOwnedVehicle(netId)
exports.nord_vehiclekeys:SetVehicleOwner(netId, identifier)
```

`RegisterOwnedVehicle` is useful after spawning a framework-owned vehicle so the resource can initialize its authoritative state.

`SetVehicleOwner` creates a runtime ownership override for trusted custom/standalone integrations.


# Client Exports

Client exports are useful for UI, vehicle interactions and custom gameplay.

```lua
local hasKey = exports.nord_vehiclekeys:HasKey(vehicleOrPlate)
local hasLocal = exports.nord_vehiclekeys:HasLocalKey(vehicleOrPlate)

exports.nord_vehiclekeys:ToggleLock(vehicle)
exports.nord_vehiclekeys:LockVehicle(vehicle)
exports.nord_vehiclekeys:UnlockVehicle(vehicle)

local vehicle, distance = exports.nord_vehiclekeys:GetClosestVehicle(10.0)
exports.nord_vehiclekeys:OpenKeyRemote(slot)
```

The theft module also exposes:

```lua
exports.nord_vehiclekeys:StartLockpick(vehicle)
exports.nord_vehiclekeys:StartHotwire(vehicle)
exports.nord_vehiclekeys:StartCarjack(vehicle, driver)
```

Physical item-use handlers are also exposed internally for inventory integration, but normal external resources should use the higher-level key exports.


# Garage Integration

A garage that spawns a player's owned vehicle should register the entity and only issue a key when the garage intentionally wants to hand one to the player.

Server example:

```lua
local netId = NetworkGetNetworkIdFromEntity(vehicle)
exports.nord_vehiclekeys:RegisterOwnedVehicle(netId)
exports.nord_vehiclekeys:GiveKeys(source, plate)
```

If the garage stores the vehicle and should remove the currently issued key:

```lua
exports.nord_vehiclekeys:RemoveKeys(source, plate)
```

Do not create an insecure client event that accepts an arbitrary `source` and `plate` and forwards them directly to `GiveKeys`.


# Dealership Integration

When a player purchases a vehicle, issue the key **after** the purchase and ownership transaction succeeds.

```lua
-- purchase validated, money removed and ownership row saved first
exports.nord_vehiclekeys:GiveKeys(source, plate)
```

When a dealership sells/transfers/removes a vehicle from a player, remove the old holder's key deliberately:

```lua
exports.nord_vehiclekeys:RemoveKeys(oldOwnerSource, plate)
```

If the new owner is online:

```lua
exports.nord_vehiclekeys:GiveKeys(newOwnerSource, plate)
```

This keeps physical key ownership explicit and avoids silent regeneration.


# Rental Integration

Rental scripts should normally use temporary keys.

Example 30-minute rental:

```lua
exports.nord_vehiclekeys:GiveTempKey(source, plate, 1800)
```

When the rental ends early:

```lua
exports.nord_vehiclekeys:RemoveKeys(source, plate)
```

For a shared company vehicle with a known duration:

```lua
exports.nord_vehiclekeys:GiveSharedKey(source, plate, 3600)
```

Choose `temporary` for a short-lived one-player rental and `shared` when the semantics are intentionally shared access.

# 8 · Database, Security & Troubleshooting

Operate the resource safely in production and resolve the most common installation issues.

# Database Schema

Auto SQL creates the resource schema automatically when persistence is enabled.

Core tables:

### `nord_vehiclekeys_keys`

Stores persisted key grants:

- plate
- holder identifier
- key type
- expiry timestamp
- created/updated timestamps

### `nord_vehiclekeys_recovery_npcs`

Stores persistent recovery NPC placement and appearance:

- label
- ped model
- scenario
- X/Y/Z
- heading
- creator identifier

### `nord_vehiclekeys_schema`

Tracks internal database schema revisions. This is a database schema number, not the resource release number.

Manual SQL is included at:

```text
sql/install.sql
```


# Security Model

Nord Vehicle Keys is designed around server authority.

## Important protections

- OneSync entity checks are required by default.
- Lock state is synchronized through entity state bags.
- Key grants/removals are validated server-side.
- Physical key metadata is re-read from the authoritative inventory slot.
- Session/stolen keys are bound to the player connection/session token.
- Carjacking actions use server-side action reservation and validation.
- Ownership lookup is server-side.
- Recovery keys are only issued after ownership validation.
- Admin actions use ACE permission checks.
- Sensitive compatibility events are disabled by default.
- Callback/action rate limits reduce spam and replay abuse.

## Trusted export rule

Exports such as `GiveKeys` should only be called by trusted server resources after that resource has validated its own business logic.


# Production Checklist

Before production deployment:

- [ ] OneSync enabled.
- [ ] Database backups available.
- [ ] `oxmysql` starts before Nord Vehicle Keys.
- [ ] Framework bridge detected correctly.
- [ ] Inventory bridge selected correctly.
- [ ] `vehicle_key` definition is non-stackable when using Nord Inventory.
- [ ] Recovery ACE configured only for staff who need it.
- [ ] Ownership table/columns match your framework schema.
- [ ] Custom bridge code returns valid values and does not trust arbitrary client payloads.
- [ ] Test owner key issue/removal.
- [ ] Test player key sharing and revocation.
- [ ] Test stolen key removal after exiting the vehicle.
- [ ] Test recovery NPC key reissue.
- [ ] Test ghost placement cancel/confirm.
- [ ] Test lock state with multiple players nearby.


# Common Issues

## Physical key exists but does not open the remote

Check that:

- `nord_inventory` starts before `nord_vehiclekeys`.
- The `vehicle_key` item is non-stackable.
- The slot metadata contains a normalized plate.
- The key is not expired.
- The physical-key item use handler is registered.

## Player keeps getting a key back after deleting it

Strict physical-key mode should use:

```lua
RequirePhysicalItemForAccess = true
OwnerAccessWithoutPhysicalItem = false
StoredAccessWithoutPhysicalItem = false
AllowClientOwnerClaim = false
```

A missing key should then only return through recovery/admin/trusted server exports.

## Recovery NPC is underground

Use the visual placement editor and save the final position. The saved Z is authoritative and the final NPC should not run a second ground snap.

## Recovery NPC is stuck in a broken scenario pose

Use a valid GTA scenario name. The resource applies the scenario without a destructive intro sequence and freezes the NPC after the final placement is applied.

## Fullscreen dark NUI overlay

The key remote NUI should be transparent and hidden while closed. If you replaced the web files, ensure `html` and `body` do not force a dark background or `color-scheme: dark`.

## "The action is no longer valid" during carjacking

Make sure all resource files are from the same build and that OneSync is enabled. The carjack completion validates the reserved vehicle/action rather than relying on the NPC remaining in the same network seat state.


# Bridge Troubleshooting

## Framework not detected

Force the bridge in `config/bridges.lua`:

```lua
Config.Bridges.Framework = 'qb' -- or qbx / esx / standalone / custom
```

## Inventory not detected

Force the bridge:

```lua
Config.Bridges.Inventory = 'nord' -- or ox / qb / esx / custom
```

If `Config.Inventory.Strict = true`, item-requiring actions are denied when no bridge can validate the item.

## Custom notification not showing

Set:

```lua
Config.Bridges.Notify = 'custom'
```

and make `NVKNotify.Custom(...)` return `true` after showing the notification.

## Custom Text UI not hiding

Both custom hooks must be implemented:

```lua
NVKTextUI.CustomShow(...)
NVKTextUI.CustomHide(...)
```

Return `true` when the action was handled.


# FAQ

## Does owning a vehicle automatically recreate a deleted key?

No, not with the default strict physical-key settings. The player must recover or be explicitly issued a new key.

## Can I use the resource without Nord Inventory?

Yes for generic access/item checks through supported bridges. The complete physical `vehicle_key` metadata workflow is native to Nord Inventory; a custom inventory can implement the provided physical-key hooks.

## Can a player recover a key for a vehicle they do not own?

No. Recovery validates ownership server-side before issuing the key.

## Do stolen keys stay forever?

No. They are designed to expire when the player leaves the stolen vehicle and also have a safety timeout.

## Can a garage or dealership give keys?

Yes. Use the trusted server export:

```lua
exports.nord_vehiclekeys:GiveKeys(source, plate)
```

## Can I replace notifications or Text UI without editing the core?

Yes. Select `custom` in `config/bridges.lua` and edit only the matching bridge file.

## Is the recovery NPC placement persistent?

Yes. NPC definitions and coordinates are stored in the database.