ns-poster
Designable, syncable in-world posters for FiveM. Players open a modern NUI editor to compose a poster (text + images, presets, custom backgrounds, categories), then place it as an interactive prop anywhere in the world. The actual design is rendered onto the prop face in the world via DUI — not just a generic prop. Nearby players inspect it (ox_target or a key prompt), like it, report bad ones, and owners can renew them before they auto-expire. Admins get a moderation queue with one-click teleport.
Framework-agnostic via ns-utils (ESX / QBCore / Qbox auto-detect, copied into the resource — no shared dependency).
Discord: https://discord.gg/UyyngemnF8 — support, bug reports, feedback.
Highlights
- Real in-world rendering — the poster you design is drawn onto the prop in the world through a DUI runtime texture (per-poster, distance-streamed).
- Modern NUI editor — drag / resize / rotate text and image elements on a 600×800 (3:4) canvas, undo/redo, fonts, image filters, presets.
- Categories — Notice / Wanted / Event / Missing Person / For Sale / Custom, each with its own blip colour and a filter in the
/posterslist. - Economy — placing (and renewing) can cost money, with per-prop and per-category overrides and an optional refund on removal.
- Permissions — restrict who can place (by job), restrict categories to jobs, allow police-style takedowns, and gate placement to zones.
- Image upload — optional; players upload a local image which the server proxies to imgur (or a custom endpoint) and inserts the URL.
- Likes / reports / TTL renew, admin moderation queue.
- Dual Discord webhooks — a private admin audit log and a public member feed, both embedding a rendered image of the actual poster (uploaded straight to Discord — no external host needed).
- MySQL or JSON persistence.
Features
Designer (NUI)
- 9 built-in presets (modern Los Santos themes) — Custom (blank), Wanted (LSPD), Club Night, For Sale, Magazine Cover, uwu Cafe, Now Hiring, Lost Pet, For Rent.
- Drag / resize / rotate text and image elements on a 600×800 (3:4) canvas.
- 8 fonts shipped: Oswald, Bebas Neue, Anton (display), Inter (sans), IM Fell English, Playfair Display (serif), Special Elite (typewriter), Pacifico (script).
- 8 image filters: None, Sepia, Grunge, Noir, B&W, Vintage, Faded, Cold.
- Image sources: built-in asset library, whitelisted-domain URLs, and (optional) local upload.
- Custom backgrounds: library, https URL, or solid colour.
- Category selector + live cost preview on the Place button.
- Undo/redo via design history; save your own composition as a personal preset.
In-world rendering (DUI)
- Each placed poster's design is drawn flat onto the prop's face as a world-space quad (
DrawSpritePoly) sampling a per-poster DUI runtime texture — truly flat and angle-independent, readable on both faces of the board. - Renders identically on a cold join and a warm restart (no model-reload or texture-streaming dependency).
- Distance-streamed: only posters within
Config.Dui.RenderDistancerender, and at mostConfig.Dui.MaxConcurrentat once (nearest first). The rest show the plain board. - A custom flat board prop (
ns_poster) is streamed with the resource. The prop and the design-size/geometry are fixed (tuned inclient/dui.lua) so the flat alignment is always correct — nothing to configure. - Set
Config.Dui.Enabled = falseto show the bare prop only (no design render).
Placement
- Uses object_gizmo for full 3D position + rotation control while placing.
- Ghost prop is local-only; the canonical networked record is spawned by the server on confirm — no duplicates.
- Cancel returns the player to the designer with the in-progress design intact.
World + sync
- Distance-based streaming — props spawn at
Config.StreamDistance(100m), despawn beyondConfig.UnstreamDistance(120m), hard capConfig.MaxActivePerClient(30). - Category-coloured map blip per poster — toggle with
Config.UseBlips. /postersopens a list with category filter + GPS waypoint to any poster.
Interaction
Config.Interaction = 'target'→ ox_target / qb-target hover menu.Config.Interaction = 'key'→ proximity[E]prompt (DrawText3D).- Target mode auto-falls back to the key prompt if no target resource is found.
Economy
Config.Economy.Enabledgates everything. Placing costsPlaceCost, renewing costsRenewCost, both viaUtils.RemoveMoney(cashorbank).- Per-prop (
PropCosts) and per-category (CategoryCosts) overrides; optionalRefundOnRemovewhen the owner removes their own poster.
Permissions
PlaceJobs— restrict who may place at all (empty = everyone).CategoryJobs— restrict specific categories to specific jobs.TakedownJobs— jobs that can remove anyone's poster (e.g. police); admins always can.AllowedZones/BannedZones— gate placement to areas.
Social
- Likes — one per player per poster, rate-limited; owners optionally blocked from liking their own (
Config.AllowSelfLike). - Reports — players flag content with a reason; per-identifier cooldown plus a per-poster duplicate guard until an admin resolves it. Owners can be allowed to report their own poster with
Config.AllowSelfReport(off by default). - Every poster has a short 4-digit ID shown to admins, searchable in the
/posterslist and referenced in reports and webhook logs.
Expiration (TTL)
- Posters auto-expire after
Config.PosterTtl(default 7 days;0= never). - Owners renew from the inspect screen (
RenewExtensionfrom now). - Server sweeps expired posters every
Config.ExpirySweepTick.
Admin moderation
/posterreportsopens the report queue (gated by ACEConfig.AdminAceor the framework admin group). One-click teleport, dismiss / resolve / remove.- Filter by Open / Resolved / Dismissed / All, re-open a report you closed by mistake, and click any thumbnail to enlarge the poster.
- Optional Discord webhook log on admin teleport.
Discord webhooks (server-only)
Two independent channels under Config.Discord — set either URL to '' to disable it:
AdminWebhook— full audit: place / renew / remove / report / admin-teleport. Each entry includes the owner (and, on reports, the reporter) with identifier, phone, Discord mention and Steam profile link when available, plus coordinates and the 4-digit poster ID.MemberWebhook— public feed: new poster / renewed / removed, showing only the content (category, author name, phone, poster ID) — no identifiers/coords.- Both embed a rendered PNG of the actual poster, uploaded straight to the webhook as a file attachment via the bundled
server/discord.js(Nodehttps+Buffer) — no imgur/external host required. SetConfig.Discord.AttachPreview = falsefor text-only embeds, orUploadMode = 'host'to push the image toConfig.Upload(imgur/custom) instead. - Per-event toggles:
LogPlace/LogRenew/LogRemove/LogReport/LogAdminTeleport(admin) andMemberOnPlace/MemberOnRenew/MemberOnRemove(member).
Steam link only appears when the server provides a
steam:identifier — addset steam_webApiKey "<key>"toserver.cfg(free key from steamcommunity.com/dev/apikey). Discord mention needs the player to have linked Discord to their FiveM account.
Persistence
- oxmysql (default,
Config.UseMySQL = true) or JSON files (server/data/). Tables auto-create on boot;sql/install.sqlis provided for manual/production installs.
Requirements
| Resource | Required | Purpose |
|---|---|---|
| object_gizmo | ✅ | 3D placement gizmo |
oxmysql | ✅ (MySQL mode) | Persistence when Config.UseMySQL = true |
ox_target / qb-target | optional | Only if Config.Interaction = 'target' |
ox_lib | optional | Used automatically for notifications if present |
| Node.js + npm | optional | Only for rebuilding the NUI |
No framework is a hard dependency. ESX / QBCore / Qbox are auto-detected at runtime by the bundled
utils/(ns-utils). Standalone also boots, but money, jobs and identity features need a framework.
Installation
Drop the
ns-posterfolder into your server'sresources/directory.Add to
server.cfgafter your framework + object_gizmo:ensure oxmysql ensure object_gizmo ensure ns-poster(Optional) grant admin moderation access:
add_ace group.admin ns-poster.admin allowMySQL mode (default) — tables auto-create on first boot, or import
sql/install.sqlfor an explicit/production install.(Optional) populate
Config.AllowedImageDomainsto allow image URLs, and/or configureConfig.Uploadto allow local image upload.
Commands & keys
| Action | How |
|---|---|
| Open designer | /poster |
| Open posters list (filter + GPS waypoint) | /posters |
| Open admin report queue | /posterreports (ACE/admin gated) |
| Inspect a poster | Hover with ox_target or walk up and press E |
| Placement gizmo | After pressing Place: left-click confirm, right-click cancel |
Opening the designer: command and/or item
Config.Command— the/postercommand (toggle / rename it).Config.Item— open the designer by using an inventory item, and optionally consume one item per placement (Config.Item.Consume). SetConfig.Command.Enabled = falsefor item-only access.- Adding the item to your inventory (ox_inventory / qb-inventory / ESX) is a one-time step — see the
install/folder.
Props & the design
Posters use a single custom flat 3:4 board prop, ns_poster, streamed with the resource (stream/). The design is drawn as a flat world-space quad (DrawSpritePoly) on the prop face, so it renders identically on a cold join and a warm restart and reads correctly from both sides.
The prop model, the design size on its face, and the quad geometry are all fixed and tuned in client/dui.lua — there's nothing to configure here, and the flat alignment is always correct.
Configuration
All settings live in config.lua. Key blocks:
- Storage —
UseMySQL,AutoSaveInterval. - Streaming + limits — distances, ticks, per-player caps.
- Placement — anti-teleport clamp (placement distances).
- Dui —
Enabled(in-world design on/off),RenderDistance,MaxConcurrent. - Categories — keys, labels, blip sprite + colour.
- Blips —
UseBlips(map blips on/off),BlipName,BlipScale. - Economy — costs, account, overrides, refund.
- Interaction —
target/key, distance, key control. - Permissions —
PlaceJobs,CategoryJobs,TakedownJobs, zones. - Image whitelist + Upload — allowed hosts, imgur/custom upload proxy.
- Expiration / renew, Social (likes, reports, self-report).
- Discord —
AdminWebhook/MemberWebhook,AttachPreview,UploadMode, per-event toggles, username/avatar, and per-event embedColors. - Messages.
Image upload setup (optional)
Config.Upload = {
Enabled = true,
Provider = 'imgur', -- 'imgur' | 'custom'
ImgurClientId = 'your-id', -- api.imgur.com/oauth2/addclient (no callback)
-- custom: CustomEndpoint accepts JSON { image = '<base64>' } → { url = '...' }
MaxBytes = 3 * 1024 * 1024,
}Upload runs server-side (base64 → JSON), so it avoids NUI CORS and FXServer multipart issues. No secrets ship in the NUI.
Building the NUI
Pre-built output ships in html/. Only rebuild if you change anything under ui/.
cd ui
npm install
npm run buildThe build emits to ../html/ and regenerates the library manifest from files in library/. The DUI render page is the same bundle loaded at html/index.html#render.
Asset library
Drop your own art into library/backgrounds/ and library/decorations/ (.svg/.png/.jpg/.webp/.gif). The picker entries are auto-generated by ui/scripts/gen-library-manifest.mjs on every build.
Security model
- Server is authoritative — every place / remove / save-preset / like / report / renew / resolve-report / upload goes through
shared/schema.luavalidation, payload-size cap, image-URL whitelist, permission/zone checks, economy charge, and (for remove/renew) ownership/takedown checks. - Placement coordinates are clamped to within
Config.PlacementMaxDistanceof the requesting player (anti-teleport). - React renders all user text via
{value}(neverdangerouslySetInnerHTML). - Image URLs must be
https://and matchConfig.AllowedImageDomains. - Per-identifier rate limits on place / like / report / renew / upload.
- All ACE / job / ownership checks run server-side.
Exports
All exports take a real player source. source = 0 (console) is rejected so a buggy/malicious peer resource can't bypass ownership.
-- Placement & lifecycle
exports['ns-poster']:placePoster(source, { design = {...}, world = { coords, heading, model } })
exports['ns-poster']:removePoster(source, posterId)
exports['ns-poster']:renewPoster(source, posterId)
exports['ns-poster']:getAllPosters() -- read-only snapshot
-- Social
exports['ns-poster']:likePoster(source, posterId)
exports['ns-poster']:reportPoster(source, posterId, reason)
-- Moderation (require admin)
exports['ns-poster']:listReports(source, filterStatus) -- 'open' | 'resolved' | 'dismissed' | nil
exports['ns-poster']:resolveReport(source, reportId, action) -- 'dismiss' | 'resolve' | 'remove' | 'reopen'