Skip to content
norbix-codePublic

About

Norbix sdk for IOS(Swift)

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

sdk-swift

NorbixSwift

CI Release Swift License

Official Swift SDK for Norbix. The SDK ships as two separate libraries that mirror the two Norbix planes:

  • NorbixApi — project-scoped data operations (default https://api.norbix.ai)
  • NorbixHub — project / account configuration operations (default https://hub.norbix.ai)

A small shared NorbixCore module holds the transport, config, and error types both clients use.

Client Modules
NorbixApiClient accessToken, ai, apiKeys, auth, database, echo, files, membership, payments, publicProjects
NorbixHubClient accessToken, account, ai, apiKeys, auth, code, database, echo, email, environments, files, logs, membership, notifications, payments, platform, regions, scheduler, webhooks

Built for Apple platforms (iOS, macOS, tvOS, watchOS, visionOS) with standard Swift tooling: Swift Package Manager, XCTest, GitHub Actions CI, and semantic-release based tagging.

Install (Swift Package Manager)

In Xcode:

  1. File -> Add Packages...
  2. Use URL: https://github.com/norbix-code/sdk-swift
  3. Pick a version rule (recommended: Up to Next Major)
  4. Add NorbixApi and/or NorbixHub to your target — only the planes you need.

Or in Package.swift:

.package(url: "https://github.com/norbix-code/sdk-swift.git", from: "1.0.0")
.target(
    name: "MyApp",
    dependencies: [
        .product(name: "NorbixApi", package: "sdk-swift"),
        // .product(name: "NorbixHub", package: "sdk-swift"),
    ]
)

Quickstart — API

import NorbixApi

let client = try NorbixApiClient(projectId: "proj_123", apiKey: "sk_live_xxx")
let _ = try await client.database.getDatabaseSchemas()
let me = try await client.membership.getCurrentUser()

Login flow:

import NorbixApi
import NorbixCore

let client = try NorbixApiClient(projectId: "proj_123")
let _ = try await client.login(.init(userName: "alice@team.io", password: "secret"))
let me = try await client.membership.getCurrentUser()

Quickstart — Hub

import NorbixHub

let hub = try NorbixHubClient(
    projectId: "proj_123",
    apiKey: "sk_live_xxx"
)
let account = try await hub.account.getAccountProfile() // no accountId needed
let integrations = try await hub.files.getFilesIntegrations()

// Sign-up, invitation, regions and verify need no token and no accountId:
let anon = try NorbixHubClient(projectId: "proj_123") // no apiKey, no bearerToken
let regions = try await anon.account.getAccountRegions()

Self-hosted / on-prem deployments

Both clients accept a baseUrl parameter. Use it for self-hosted Norbix or local development:

// Custom company domain
let client = try NorbixApiClient(
    projectId: "proj_123",
    apiKey: "sk_live_xxx",
    baseUrl: "https://api.norbix.isidos.lt"
)

// Local development
let hub = try NorbixHubClient(
    projectId: "proj_123",
    apiKey: "sk_live_xxx",
    baseUrl: "http://localhost:5000"
)

If you do not pass baseUrl, the SDK uses Norbix' production endpoints: https://api.norbix.ai and https://hub.norbix.ai.

Multi-region support

Norbix projects can live in more than one region. The SDK targets a region by sending its code (e.g. nb-eu-germany) in the nb-region header. There is no default region: when no region is configured, no header is sent and the backend picks its own default.

Selecting a region

Pass region when constructing either client:

let client = try NorbixApiClient(
    projectId: "proj_123",
    apiKey: "sk_live_xxx",
    region: "nb-eu-germany"
)

let hub = try NorbixHubClient(
    projectId: "proj_123",
    apiKey: "sk_live_xxx",
    region: "nb-eu-germany"
)

Or set the NORBIX_REGION environment variable — the convenience initializers and NorbixConfig.fromEnvironment fall back to it when no region argument is given:

NORBIX_REGION=nb-eu-germany
NORBIX_LANGUAGE="lt-LT, en;q=0.8"   # optional: sent as Accept-Language

Switching at runtime

Both clients expose setRegion(_:) and a region getter:

hub.setRegion("nb-eu-germany")
print(hub.region ?? "unset") // "nb-eu-germany"

hub.setRegion(nil)           // unset: no nb-region header is sent again
print(hub.region ?? "unset") // "unset"

Passing nil (or an empty string) unsets the region. Unlike setEnv there is no value that means "the default region" — unset simply omits the header.

Per-call override

The hub.regions methods accept a region: argument that overrides the client's region for that single request. The override affects the nb-region header only — it never changes the request URL:

let regions = try await hub.regions.getAccountRegions([:], region: "nb-us-east")

Regional base URLs

When a client uses an SDK-default base URL, configuring a region (at init or via setRegion) also composes the regional endpoint by prefixing the region code as a subdomain:

  • https://api.norbix.ai → https://nb-eu-germany.api.norbix.ai
  • https://hub.norbix.ai → https://nb-eu-germany.hub.norbix.ai

setRegion(nil) restores the default URL. A custom baseUrl (self-hosted / on-prem / local development) is never rewritten: the nb-region header is still sent, but the URL stays exactly as you configured it.

Managing a project's regions (Hub)

hub.regions wraps the regions endpoints. getAccountRegions needs no token and no accountId (the gateway route is anonymous); updateProjectRegions works with a token only (the client needs no accountId):

// GET /v2/account/regions — regions available to the account.
// Response carries "items"; each item has "id" (the region code,
// e.g. "nb-eu-germany"), "continent", and "name".
let available = try await hub.regions.getAccountRegions([:])

// PATCH /v2/account/projects/{projectId}/settings/regions
// Pass nil to leave a field unchanged. Empty response on success.
_ = try await hub.regions.updateProjectRegions(
    projectId: "proj_123",
    primaryRegion: "nb-eu-germany",
    additionalRegions: ["nb-us-east"]
)

The SDK is untyped on the wire, so region fields on other endpoints pass straight through — for example, creating a project with a primary region:

_ = try await hub.account.createProject([
    "primaryRegion": "nb-eu-germany"
    // ...other project fields
])

Language

The gateway picks the response language from the standard Accept-Language header only (q-values are honoured; the project's default language applies when nothing matches) and names the language it used in the Content-Language response header. There is no default language: when none is configured, no header is sent.

Pass language when constructing either client — a single tag or a full q-value list, sent verbatim:

let client = try NorbixApiClient(
    projectId: "proj_123",
    apiKey: "sk_live_xxx",
    language: "lt"
)

let hub = try NorbixHubClient(
    projectId: "proj_123",
    apiKey: "sk_live_xxx",
    language: "lt-LT, en;q=0.8"
)

Or set the NORBIX_LANGUAGE environment variable — the convenience initializers and NorbixConfig.fromEnvironment fall back to it when no language argument is given:

NORBIX_LANGUAGE="lt-LT, en;q=0.8"

Switch at runtime with setLanguage(_:) and read it back with language; nil (or an empty string) unsets it, so no header is sent again:

hub.setLanguage("en")
print(hub.language ?? "unset") // "en"
hub.setLanguage(nil)           // unset: no Accept-Language header

Wherever a per-call region: exists (the hub.regions methods and the Transport.send / downloadData calls) a per-call language: overrides the client's language for that single request:

let regions = try await hub.regions.getAccountRegions([:], language: "en")

The old cultureCode request field and the CultureCode / norbix-language headers are no longer read by the gateway; use language.

Real-world examples

1) Fetch recent orders (API)

import NorbixApi
import NorbixCore

let client = try NorbixApiClient(projectId: "proj_123", apiKey: "sk_live_xxx")

do {
    let response = try await client.database.find([
        "collectionName": "orders",
        "take": 20,
        "skip": 0,
    ])
    if let payload = response as? [String: Any] {
        print("orders response:", payload)
    }
} catch let error as NorbixError {
    print(error.code, error.status, error.message)
}

2) Login then call user endpoint (API)

import NorbixApi

let client = try NorbixApiClient(projectId: "proj_123")
let auth = try await client.login(.init(userName: "alice@team.io", password: "secret"))
print("logged in:", auth["bearerToken"] ?? "")

let me = try await client.membership.getCurrentUser()
print(me)

3) Account Hub call (token only)

Account calls take the account from the signed-in token, so the client needs no accountId.

Four account calls need no token at all (scope .unauthenticated, no Authorization header): createAccount (sign-up), createTeamMemberFromInvitation, getAccountRegions (also hub.regions.getAccountRegions) and verifyAccount. verifyAccount takes the account id once, in the request: pass accountId and token from the verification email link; they go in the query.

import NorbixHub

let hub = try NorbixHubClient(
    projectId: "proj_123",
    apiKey: "sk_live_xxx"
)

let account = try await hub.account.getAccountProfile()
print(account)

// No token, no accountId:
let anon = try NorbixHubClient(projectId: "proj_123")
_ = try await anon.account.verifyAccount(["accountId": "acc_456", "token": codeFromEmail])

4) Linked records with a typed answer (API)

A record read with expandReferences: true answers every reference (a user, a role, a taxonomy term, a record of another collection, a file) as { id, display }. Declare the field as ExpandedReference from NorbixCore:

import NorbixApi
import NorbixCore

struct Post: Codable, Sendable {
    let title: String
    let author: ExpandedReference
    let tags: [ExpandedReference]
}

let page: Page<Post> = try await client.database.find(
    collection: "posts", expandReferences: true, as: Post.self
)
print(page.items[0].author.displayText() ?? "?")   // Jane Doe

More in API · Database → Linked records and Files by id.

Authentication

  • API key mode: apiKey or NORBIX_API_KEY
  • Bearer mode: bearerToken, NORBIX_BEARER_TOKEN, or client.login(...)
  • If both set, bearer token wins
  • If none set, the SDK throws NORBIX_NOT_AUTHENTICATED on the first authenticated call
  • Anonymous calls send no Authorization header, even when the client has a token: hub.account.createAccount, createTeamMemberFromInvitation, getAccountRegions, verifyAccount, and hub.regions.getAccountRegions

Preview a notification with its signed link (no sign-in)

The push, email and SMS preview routes open with the signed link (hash) alone — no API key or bearer token needed. When the client does have a token it is still sent (scope .optional).

let hub = try NorbixHubClient(projectId: "proj_123") // no apiKey, no bearerToken
let preview = try await hub.notifications.previewEmailNotification(["hash": signedLink])
// also: previewPushNotification(...), previewSmsNotification(...)

A signed-in member can pass projectId + notificationId instead of hash. A bad or expired link throws a NorbixError with status 401.

SMS campaigns to account users

An SMS campaign can go to the account's own people — the owner and team members (deliveryType: "AccountUsers"). A member without a phone number is skipped; each member saves their own number with updateMyAccountUserPhone. The SMS template content has a body only (no subject — the sender is the integration), and getSmsCampaigns takes an optional campaignId filter.

let hub = try NorbixHubClient(projectId: "proj_123", bearerToken: token) // no accountId needed
_ = try await hub.notifications.createSmsCampaign([
    "templateId": "tmpl_1",
    "integrationId": "nbin_1",
    "deliveryType": "AccountUsers",
    "accountUsers": [
        "recipientsSourceType": "AccountUsers",
        "recipients": ["usr_owner", "usr_member"],
        "campaignTime": 1767225600, // unix seconds, UTC
    ],
])

// Your own team-member record (not the organisation profile) and your own phone.
let me = try await hub.account.getMyAccountUserProfile()            // GET /{version}/account/me
_ = try await hub.account.updateMyAccountUserPhone(["phone": "+37060000000"]) // PUT /{version}/account/me/phone; "" clears it

The one-message calls getSmsCampaignMessage and getEmailCampaignMessage (GET …/campaigns/{campaignId}/messages/{id}) are gone: the gateway no longer serves that route. Use getSmsCampaignMessages / getEmailCampaignMessages (the list), or the batch notification calls.

Triggers: order and break on failure

When several triggers fire for the same event they run as a queue. Two optional fields on trigger (every save: schema, membership, files and payments triggers) shape it: order — a whole number, 0 or more; lower runs earlier, a trigger without an order runs after the numbered ones, equal places run by name — and breakOnError — true stops the later triggers of the same event when this trigger's action fails (default false). Both come back on the get and list calls; a negative order fails with CM-ERRORS-TRIGGERS-008.

_ = try await hub.membership.saveMembershipTrigger([
    "trigger": [
        "type": "Membership",
        "name": "welcome first",
        "when": "OnRegistered",
        "isEnabled": true,
        "order": 1,
        "breakOnError": true,
        "action": ["type": "WebhookCall"],
    ],
])

Code — cloud functions

Norbix does not host code: a trigger's Code action, a scheduler task or a manual call runs a function in your cloud (AWS Lambda, Azure Functions, Google Cloud Functions) through a Code integration. hub.code covers every Code Hub endpoint (38); the five cloud-function routes list the functions an integration may call, their aliases and versions, invoke one by hand, give a sample envelope for a local run and the last runs of a trigger or task — getCodeFunctions, getCodeFunctionQualifiers, invokeCodeFunction, getCodeFunctionEventSample, getCodeFunctionRuns.

let functions = try await hub.code.getCodeFunctions(["integrationId": "int_1", "prefix": "orders-"])
let result = try await hub.code.invokeCodeFunction([
    "functionRef": ["integrationId": "int_1", "functionId": "orders-on-update", "qualifier": "prod"],
    "payload": ["orderId": "6ac54a59212c29912eaddd81", "dryRun": true],
    "invocationType": "Sync",
])

Details and the envelope the function receives: docs/hub/code.md.

Environment configuration

NORBIX_PROJECT_ID=proj_123
NORBIX_API_KEY=sk_live_...
NORBIX_BEARER_TOKEN=...
NORBIX_ACCOUNT_ID=acc_456   # optional: no call needs it
NORBIX_API_URL=https://api.norbix.ai
NORBIX_HUB_URL=https://hub.norbix.ai
NORBIX_API_VERSION=v3
NORBIX_HUB_VERSION=v3
NORBIX_REGION=nb-eu-germany

You can also build a config explicitly:

import NorbixCore
import NorbixApi

let cfg = try NorbixConfig.fromEnvironment(target: .api)
let client = NorbixApiClient(config: cfg)

Errors

Every failure is a NorbixError from NorbixCore:

import NorbixCore

do {
    _ = try await client.database.find(["collectionName": "orders"])
} catch let error as NorbixError {
    switch error.code {
    case "NORBIX_NOT_AUTHENTICATED":      /* re-login */ break
    case "NORBIX_ACCOUNT_SCOPE_REQUIRED": /* set accountId (no built-in call needs it) */ break
    case "NORBIX_NETWORK_ERROR":          /* retry */ break
    default:                              print(error.localizedDescription)
    }

    // httpStatus / errorCode are the names every Norbix SDK uses.
    // status / code are the same values, kept for older code.
    print(error.httpStatus, error.errorCode, error.message)
    for item in error.errors {
        print(item.errorCode ?? "", item.fieldName ?? "", item.message ?? "")
    }
    print(error.body ?? "") // the answer exactly as it arrived
}

message and errorCode are the gateway's own. The gateway puts them inside responseStatus.errors[], so the SDK reads that list first, takes the first entry for the message and the code, and keeps every entry in errors. Only when the body has no responseStatus are the top-level message and errorCode read. Request failed (HTTP N) with the code HTTP_<status> is the last fallback, used when the body says nothing — a 500 page that is not JSON, say.

The Database calls follow rules the gateway checks (bulk writes with an empty filter need "allRecords": true, $ operators in an update body are refused, one schema-trigger copy per env) — the rules and each refusal's error code are in docs/database-rules.md.

Breaking change — a refused call now throws

The gateway answers a business refusal (an unknown id, a rule that says no) with HTTP 200 and responseStatus.isSuccess = false. The SDK used to hand that answer back as a normal value, so code carried on as if the call had worked. It now throws a NorbixError with httpStatus 200 and the gateway's message and error code.

If your code checked the responseStatus of the answer itself, move that check into a catch. downloadData (file download, the public file link) answers with raw bytes rather than a document, is not JSON, and is unchanged.

SDK maintenance

API/Hub wrappers, tests, and module docs are generated by the SDK maintenance workflow.

Development

swift test
swift build

Versioning

The major version is frozen at v1 until the public launch.

  • A breaking change is released as a minor (for example v1.2.0 → v1.3.0), never as a new major.
  • Write it as feat(<scope>): <what> and add a line Breaking: <what changed and what callers must do> in plain words, in the pull-request body and in the commit message.
  • Never mark it the conventional-commits way: no ! in the title (feat!:), no BREAKING CHANGE footer. The PR title check fails a pull request that does.
  • As a safety net, the release config (.releaserc.json → releaseRules) maps breaking commits to a minor, so one that slips through still does not bump the major.

Release model

Push to main, next, or beta:

  • run CI
  • semantic-release calculates version from commits
  • create git tag + GitHub release

Swift Package Manager consumers get new versions from git tags/releases.

End-user AI chat and project AI settings

client.ai (NorbixApi) is the end-user AI chat for a signed-in project user: availability, sessions, entries, feedback, attachments, memory and startEndUserChatTurn, which answers at once with a turnId. The answer streams over the gateway's SSE endpoint on the user's own channel ai-chat:{projectId}:{authId} (events ai.chat.turn.*, ai.chat.session.*); a subscription to another user's channel is refused with 403 and responseStatus.errorCode = "AiChatChannelRefused" before the stream starts — do not retry it. This SDK has no SSE client. Path tokens are read from the request dictionary.

let turn = try await api.ai.startEndUserChatTurn(["sessionId": sessionId, "message": "What can you do?"])
let entries = try await api.ai.getEndUserChatEntries(["SessionId": sessionId])

Project owners configure the assistant on the Hub: hub.account (getProjectAiSettings, updateProjectAiSettings, createProjectAiAssistant, updateProjectAiAssistant, deleteProjectAiAssistant, getProjectAiUsage, setAdminPortalEnabled) and hub.ai (getEmbeddingIntegrations, saveEmbeddingIntegration, getEmbeddingIntegration, deleteEmbeddingIntegration, testEmbeddingIntegration, setLlmIntegrationAsDefault). Tables: docs/api/ai.md, docs/hub/ai.md, docs/hub/account.md.

Project settings, public config, MCP endpoint and AI service users

  • hub.account: updateProjectAdminUrl, updateProjectLegalDocuments, updateProjectExposeLegal, updateProjectExposeBrand, updateProjectExposeAuth, getAdminPortalStructure, assignAdminPortalServiceUser.
  • api.publicProjects: getPublicProjectConfig, getPublicProjectLegal — no sign-in; sent with no Authorization header.
  • hub.account, AI service users: createAiServiceUser, listAiServiceUsers, rotateAiServiceUserKey, revokeAiServiceUserKey, deleteAiServiceUser.
  • hub.account, developer MCP endpoint: sendMcpMessage, openMcpStream, endMcpSession. They return an McpResponse that carries the session id from the Mcp-Session-Id answer header.
_ = try await hub.account.updateProjectLegalDocuments(["projectId": projectId, "termsMarkdown": "# Terms"])
_ = try await hub.account.updateProjectExposeLegal(["projectId": projectId, "exposed": true])
_ = try await hub.account.updateProjectExposeBrand(["projectId": projectId, "exposed": true])
_ = try await hub.account.updateProjectExposeAuth(["projectId": projectId, "exposed": true])
let terms = try await api.publicProjects.getPublicProjectLegal(projectId: projectId, kind: "terms")

Tables and the MCP example: docs/hub/account.md, docs/api/public_projects.md.

About

Norbix sdk for IOS(Swift)

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages