AbsoluteJS

Auth, Sync & HTTP

Install @absolutejs/auth and your app signs users in. Add @absolutejs/sync and it keeps their data encrypted on the device and works offline. Your page code stays exactly as it is on the web.

#Sign-in

When @absolutejs/auth is in your package.json, the app becomes a signed-in client of your server. There is nothing to add to absolute.config.ts.

1
The user taps Sign in
Your page calls the same @absolutejs/auth client it uses on the web.
2
Your sign-in page opens
The app opens your own sign-in page in the system browser (Safari or Chrome), never in a web view, using the OAuth code flow with PKCE.
3
The app receives the session
The browser returns to the app through its private link. The app exchanges the one-time code and stores the refresh credential in the iOS Keychain or Android Keystore.
4
Everything is authenticated
From then on, page requests, @absolutejs/http calls and Sync sockets to your server are signed in automatically. Your page code never sees a token.
TS
import { createAuthClient } from '@absolutejs/auth/client';

const authClient = createAuthClient();

// In the app this opens your sign-in page in the system browser,
// with the email filled in, and returns once the user is signed in.
await authClient.signIn.email({ email, password });

const { data } = await authClient.status();
const signedIn = Boolean(data?.user);

await authClient.signOut();

AbsoluteJS registers the app with your server for you:

Client IDabsolutejs-native:<appId>, a public client with no secret, registered on your server automatically by absolute dev, start and compile.
Redirect URI<scheme>://auth/callback. The scheme is deepLinks.scheme, or your appId in lower case.
IssuerYour mobile.server.productionOrigin. Tokens are only ever sent there.
Scopesopenid and profile.

#Server setup

The app signs in through Auth’s OpenID Connect provider, so turn on oidc in your auth config. Give it a socketTicketStore when you use Sync: the app opens its socket with a short-lived, single-use ticket instead of putting a token in the URL.

TS
import { auth } from '@absolutejs/auth';

const authApplication = await auth({
  getUser,
  providersConfiguration,
  oidc: {
    // ...your issuer configuration
    socketTicketStore // lets Sync open authenticated sockets from the app
  }
});
The build checks this
A mobile build stops with a message if @absolutejs/auth is installed but its OIDC provider is not mounted, so you find out before a release, not from users.

#API calls

For plain calls to your own API, use @absolutejs/http. It works the same in the browser and the app, and it is how the signed-in user’s credentials reach your routes from the app.

TS
import { AbsoluteHttpError, http } from '@absolutejs/http';

type Order = { id: string; total: number };

try {
  const orders = await http.get<Order[]>('/api/orders');
  render(orders);
} catch (error) {
  if (error instanceof AbsoluteHttpError && error.status === 401) showSignIn();
  else throw error;
}
One originRequests go only to your production origin. Paths are relative; another host is refused.
Credentials handledIn the app, the signed-in user’s token is added for you. In the browser, your session cookie is sent to the same origin.
Nothing to leakAuthorization, Cookie and Proxy-Authorization headers set by page code are rejected, and redirects are refused, so credentials cannot leak.
Familiar methodsget, post, put, patch and delete parse JSON; request returns the raw Response; fetch plugs into Eden Treaty.

#Offline data

Your app’s screens start offline, but each page asks your server for its data. Data that must be there without a connection belongs in @absolutejs/sync. With Auth and Sync both installed, every Sync client in your pages gets a durable, encrypted store on the device. You write the same code as on the web, or use the @absolutejs/sync/react, /svelte, /vue and /angular bindings.

Encryption
AES-256-GCM
Keys held by the Keychain or Android Keystore
Background sync
15min
The OS runs a sync while the app is closed
Server effect per write
1
A queued write reaches the server once, even after retries
TS
import { createSyncCollection } from '@absolutejs/sync/client';

const orders = createSyncCollection({
  url: 'wss://shop.example.com/sync/ws',
  collection: 'orders',
  params: { status: 'open' }
});

orders.subscribe((state) => render(state.data));

// Applied on screen immediately, queued while offline,
// and reconciled with the server when the app reconnects.
await orders.mutate({
  name: 'createOrder',
  args: { total: 42 },
  optimistic: (draft) => draft.set({ id: tempId, total: 42, status: 'open' })
});
Stored on the deviceCollections and queued writes are kept in encrypted SQLite on the device, so the app shows its data and accepts changes with no connection.
One store per accountEach signed-in account has its own store. Switching accounts restarts the app’s view so nothing from the previous account stays on screen.
Reconnects by itselfWhen the app comes back to the foreground or the network returns, Sync reconnects and sends the queue.
Syncs in the backgroundAndroid’s WorkManager and iOS background tasks send queued writes and pull changes about every 15 minutes, without starting your app’s code.

#What is stored

Describe your offline data under absolutejs.sync.localSchema in package.json. Packages you install can declare their own, and the app combines them. Without one, your app’s data is version 1 with no rules.

JSON
{
  "absolutejs": {
    "sync": {
      "localSchema": {
        "version": 2,
        "migrations": [
          {
            "toVersion": 2,
            "operations": [
              { "type": "rename-field", "collection": "orders", "from": "sum", "to": "total" }
            ]
          }
        ],
        "localData": {
          "collections": [
            {
              "match": "orders",
              "sensitivity": "private",
              "protection": "required",
              "onProtectionUnavailable": "memory-only",
              "evictionPriority": "critical"
            },
            {
              "match": "catalog*",
              "sensitivity": "public",
              "protection": "none",
              "evictionPriority": "disposable",
              "maxAgeMs": 604800000
            }
          ],
          "mutations": [
            {
              "match": "createOrder",
              "sensitivity": "private",
              "protection": "required",
              "conflict": { "strategy": "client-wins", "maxAttempts": 5 }
            }
          ],
          "maxBytesPerNamespace": 52428800
        }
      }
    }
  }
}
RuleMeaning
matchA collection or mutation name, or a pattern with *.
sensitivitypublic, private or secret. Private and secret data must be protected or memory-only.
protectionrequired encrypts it on the device; none stores it as is.
onProtectionUnavailableerror, or memory-only to keep it only in memory where the device cannot encrypt, such as some browsers.
persistencedurable (the default) or memory-only.
evictionPrioritycritical, normal or disposable: what is removed first when the store is full. Queued writes are never removed.
maxAgeMsDrop a cached collection older than this.
conflictFor mutations: manual (keep it for you to resolve, the default), server-wins, or client-wins with maxAttempts.
maxBytesPerNamespaceA size limit for each account’s store.

#Schema changes

Raise version when the shape of stored data changes, and add a migration whose toVersion is the new version. The device runs every step from the version it has to the new one, once, before your pages read the store, so each version needs its migration. A release upgrades data stored by up to two earlier versions; set minimumCompatibleVersion to reach further back. Data older than that reports SCHEMA_TOO_OLD through absolute:sync-schema.

OperationFields
rename-fieldcollection, from, to
remove-fieldcollection, field
set-defaultcollection, field, value
delete-collectioncollection

#Status and failures

Two window events report what Sync is doing: absolute:sync-status for the connection and queue, and absolute:sync-schema for the stored data’s version.

TS
addEventListener('absolute:sync-status', (event) => {
  const { connection, pending, deadLetters } = event.detail;
  showSyncBadge({ connection, pending, failed: deadLetters });
});

addEventListener('absolute:sync-schema', (event) => {
  if (event.detail.state === 'failed') showUpdateRequired(event.detail.code);
});

A write the server rejected is kept rather than lost. Inspect it, retry it, send it again with new arguments, or discard it:

TS
import { getAbsoluteMobileSyncRemediation } from '@absolutejs/absolute/mobile';

const remediation = getAbsoluteMobileSyncRemediation();
if (remediation) {
  const { deadLetters } = await remediation.inspect();
  for (const failed of deadLetters) {
    if (failed.kind === 'retryable') await remediation.retry(failed.operationId);
    else if (failed.kind === 'conflict') showConflict(failed);
  }
}

#Packages

The build installs the native packages for you. Push notifications use the same sign-in; see Push notifications.