Skip to content

Custom app server API ​

A custom app can have a server API: a Lua script that runs on the CoCoCo server. Use it for work that should not happen in the browser, such as combining several API calls into one, validating input before a write, or keeping data that belongs to the app.

Export functions, call them with rpc() ​

Define each callable function on the global exports table:

lua
function exports.greet(name)
  return "Hello, " .. name .. "!"
end

Call it from the app's script with rpc(method, ...args):

js
const greeting = await rpc('greet', 'World') // "Hello, World!"
  • The arguments of rpc() become the Lua function's parameters, in order.
  • The function's return value is what the promise resolves to. Return JSON-compatible values: tables, strings, numbers, booleans or nil.
  • A method name may contain letters, digits and underscores, must not start with a digit, and can be at most 64 characters long.
  • If the function raises an error (error("...")), or the name is not exported, the promise rejects with an Error whose message says what went wrong.
  • rpc() rejects straight away if the app has no server API (APP_CONTEXT.hasServerApi is false).

How a call runs ​

  • Each call starts fresh. The script is loaded again for every call, so values you keep in Lua variables between calls are lost. Store anything that must last in the app's data container (below).

  • It runs as the signed-in user. ctx.graphql calls carry that user's authorization and tenant, so the server API cannot read or change anything the user could not.

  • Service calls return two values. Functions that reach a platform service return ok, result. When ok is false, result is the error message. Bind both values and check ok before using the result:

    lua
    function exports.jobName(jobId)
      local ok, res = ctx.graphql.query([[
        query GetJob($id: JobID!) { getJob(id: $id) { name } }
      ]], { id = jobId })
      if not ok then error(res) end
      local data = res.data
      if data == nil then error("no data returned") end
      local job = data.getJob
      if job == nil then error("job not found") end
      return job.name
    end

Store app data ​

ctx.dataContainer is persistent key-value storage that belongs to the app. A server API can reach only its own app's records.

FunctionResult
ctx.dataContainer.get(name)ok, record: the stored record, or nil if there is none. The value is in record.data.
ctx.dataContainer.put(name, value)ok, record: creates or replaces the record
ctx.dataContainer.delete(name)ok, result: removes the record

Each record is a CustomDataContainerState.

Example: shift notes ​

This server API keeps a list of notes and counts jobs by status:

lua
local NOTES_KEY = "notes"

local function loadNotes()
  local ok, record = ctx.dataContainer.get(NOTES_KEY)
  if not ok then error(record) end
  if record == nil then return {} end
  return record.data or {}
end

function exports.listNotes()
  return loadNotes()
end

function exports.addNote(text)
  if type(text) ~= "string" or text == "" then
    error("text must be a non-empty string")
  end
  local notes = loadNotes()
  table.insert(notes, { text = text, at = ctx.time.nowIso() })
  local ok, err = ctx.dataContainer.put(NOTES_KEY, notes)
  if not ok then error(err) end
  return notes
end

function exports.countJobs(status)
  local ok, res = ctx.graphql.query([[
    query CountJobs($status: String!) {
      listJobs(first: 1, filter: { status: { eq: $status } }) { totalCount }
    }
  ]], { status = status })
  if not ok then error(res) end
  if res.errors then error(res.errors[1].message) end
  local data = res.data
  if data == nil then error("no data returned") end
  return data.listJobs.totalCount
end

The GraphQL it sends:

graphql
query CountJobs($status: String!) {
  listJobs(first: 1, filter: { status: { eq: $status } }) {
    totalCount
  }
}

The client script and template that use it:

js
const notes = ref([])
const draft = ref('')
const confirmed = ref(0)
const errorMessage = ref('')

async function load() {
  errorMessage.value = ''
  try {
    notes.value = await rpc('listNotes')
    confirmed.value = await rpc('countJobs', 'CONFIRMED')
  } catch (error) {
    errorMessage.value = error.message
  }
}

async function addNote() {
  if (!draft.value.trim()) return
  try {
    notes.value = await rpc('addNote', draft.value.trim())
    draft.value = ''
  } catch (error) {
    errorMessage.value = error.message
  }
}

onMounted(load)

const setupReturn = { notes, draft, confirmed, errorMessage, addNote }
html
<div class="p-6 space-y-4">
  <p class="text-sm text-base-content/70">Confirmed jobs: {{ confirmed }}</p>
  <div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
  <div class="flex gap-2">
    <input v-model="draft" class="input input-bordered flex-1"
           placeholder="Add a note" @keydown.enter="addNote" />
    <button class="btn btn-primary" @click="addNote">Add</button>
  </div>
  <ul class="space-y-2">
    <li v-for="note in notes" :key="note.at" class="card bg-base-100 p-3">
      <span>{{ note.text }}</span>
      <span class="text-xs text-base-content/50">{{ note.at }}</span>
    </li>
  </ul>
</div>

What else the server API can use ​

Besides ctx.graphql and ctx.dataContainer, the server API has the same Lua environment as other CoCoCo scripts. It includes the extended string, table and time helpers, ctx.json, ctx.time, ctx.sql (read-only reporting SQL), ctx.cache, ctx.config, ctx.notify and ctx.script.load. See the Lua API overview.

To check a script against exactly what is available, pass it to the validateLua mutation with role: CUSTOM_APP. It reports unknown functions and type errors without saving anything.

Engine versions ​

New apps run on engine version 2, which is what this page describes: exported functions on exports, called with rpc(). Apps created on engine version 1 used an older script shape. The app's engineVersion field tells you which one it runs, and setCustomAppEngineVersion upgrades an app to version 2.

Connect machines, automate workflows, and build custom apps for print production.