dg.run

Sign-in and database

Your app is static files, but it can still have users and saved data. The platform provides two services on your app's own address, under /_dg/:

  • Sign in with dg.run: users log in (or sign up) on dg.run and connect your app. You never handle passwords or email codes.
  • A small NoSQL database: JSON documents grouped in collections, private per user or shared with everyone.

There are no keys or settings. The platform knows which app is calling from the address it runs on.

Install

npm install @dgrun/sdk
import { createDg } from '@dgrun/sdk'

const dg = createDg()
await dg.ready // finishes a sign-in if the user just came back from dg.run

if (!dg.user) {
  button.onclick = () => dg.signIn()
} else {
  console.log(dg.user.email)
}

signIn() sends the browser to dg.run. The user logs in, sees what your app asks for, taps Allow, and lands back on your app signed in. If they already connected your app, they come straight back without being asked again.

dg.user is { id, email }. The id is stable for that user in your app and different in every other app.

Store data

const todos = dg.db.collection('todos') // private to each user

const doc = await todos.put({ title: 'Buy milk', done: false }) // new document, random key
await todos.put({ title: 'Eggs', done: false }, 'eggs')          // your own key: create or replace
await todos.patch('eggs', { done: true })                        // change some fields
const { items, hasMore } = await todos.list({
  where: [{ field: 'done', value: false }],
  orderBy: 'createdAt',
  desc: true,
  limit: 20,
})
await todos.get('eggs')
await todos.delete('eggs')

Every document comes back as { key, data, owner, createdAt, updatedAt }.

Private and public collections

collection('x') (default) collection('x', 'public')
Who can read only the user who created it everyone using your app, signed in or not
Who can create signed-in users signed-in users
Who can change or delete the owner the owner only
Same key for two users two separate documents one document, owned by whoever created it first

Use public collections for things like a shared board or leaderboard. list({ mine: true }) returns only the signed-in user's own public documents, and owner on each document lets you show who wrote it.

These rules are enforced by the platform, not by your code, so a user can't get around them with the browser's developer tools. For the same reason there is no "admin" role: if you need data only you can change, it doesn't fit here yet.

Queries

where takes up to 5 conditions on top-level fields of your data: eq (default), ne, gt, gte, lt, lte, and in (with a list of values). Values are strings, numbers, booleans or null. orderBy is createdAt, updatedAt or a top-level field. Use limit (up to 100) and skip to page.

Roles and permissions

Give users permission tags (like editor, admin or billing.read) and say, per collection, which tags may read, create, update and delete. The dg.run server enforces every rule on every request, so nothing depends on your app's own code, and a user can't get around it by editing your app.

Set it up in My apps → Data and users → Access:

  • Default permissions: tags every user gets unless you set theirs individually. Under Users you can give one person their own list (an empty list means no tags); "use the app default" takes them back. Changing a default reaches everyone who follows it.
  • Collection rules: for each collection, a list of grantees per operation: * (anyone, even signed out), user (a signed-in user holding the user tag: everybody has it by default, but you can take it off the app's default tags or off one person's tags; without it they stay signed in and keep their own records but match no rule that grants user), @owner (the creator of that record) or any tag. The same lists apply to every record. A collection with no rules behaves as it always has (private collections belong to each user; public ones are readable by anyone, created by any signed-in user and changed only by their creator).
  • Protected fields: name fields that only some grantees may set or change, in every record, without touching each record. For example amount: admin lets anyone who may update a transaction edit its note, but only an admin can change the amount. A write that touches a protected field without permission fails as a whole with field_forbidden; sending the same value again is fine.
  • Record permissions (optional): switch it on and every record can carry its own acl of who may read, update or delete it: a tag, *, user, or one person (user:<id>, the id you see as doc.owner).

Someone is allowed to write a record when any layer says yes (the collection's rule, or the record's own acl); protected fields then narrow which fields they may change. Examples:

// Articles: everyone reads, editors write, only admins delete. Set once in the dashboard:
//   read: * · create: editor · update: editor · delete: admin
const articles = dg.db.collection('articles', 'public')
await articles.put({ title: 'Hello' }, 'hello')        // 'forbidden' unless the user is an editor

// Support tickets: private to their author, but staff with the `support` tag see and answer all of them.
//   read: @owner, support · update: @owner, support · delete: @owner
const tickets = dg.db.collection('tickets')
await tickets.list()                                   // support: everyone's tickets; others: only their own
await tickets.list({ mine: true })                     // only mine
await tickets.patch('t1', { answer: 'Done' }, { owner: ticket.owner })   // someone else's ticket, if the rules allow it

// Transactions: users edit their own notes, but only an admin may set or change the amount.
//   read: @owner, admin · update: @owner, admin · fields: amount -> admin
const tx = dg.db.collection('transactions')
await tx.patch('t1', { note: 'lunch' })                // fine for the creator
await tx.patch('t1', { amount: 1 })                    // DgError 'field_forbidden' (error.data.field === 'amount') unless admin

// Sharing one record: with a person, with a tag, with everyone (collections with record permissions).
await docs.put({ text: '…' }, 'plan', { acl: { read: [`user:${friend.owner}`, 'reviewer'], update: ['reviewer'] } })

A person who isn't allowed gets forbidden (signed in) or unauthorized (signed out). A record you may not read looks exactly like one that doesn't exist. Whoever updates a record never takes it over: its creator stays the owner.

Showing the right screens, even offline

dg.user.permissions lists the user's tags, and await dg.can('editor') / await dg.permissions() read them from a signed token that is checked on the device with the server's public key, so they work with no network and can't be edited on the device. The tags are signed into the sign-in tokens beside the user's app-specific id (dg.user.id).

This is for showing or hiding UI only. The server never trusts the token: it reads the user's tags from its database on each request, so a tag you remove stops working at once. On a device that stays offline, the signed copy keeps showing the old tags until it expires (at most 15 days) or the device next goes online.

Limits

Document size 16 KB of JSON
Documents per user, per app 1,000
Documents per app 10,000
Request size 64 KB
Nesting 8 levels; field names can't start with $ or contain .
Names collections: lowercase letters, digits, - and _ (up to 40); keys: letters, digits, . _ : - (up to 128)

When a limit is hit the call fails with quota_exceeded or doc_too_large.

Errors

Every failed call throws a DgError with a code: unauthorized (sign in first, or the session ended), forbidden (your permissions don't allow it), not_found, invalid_data, invalid_query, doc_too_large, quota_exceeded, app_not_found, and for roles acl_disabled / invalid_acl.

If the user disconnects your app or is blocked, their next call fails with unauthorized and the SDK signs them out locally.

TypeScript

The SDK ships its types. Describe what you store and every call is checked against it:

interface Note { text: string; done: boolean }
const notes = dg.db.collection<Note>('notes')

await notes.put({ text: 'Buy milk', done: false })
await notes.list({ where: [{ field: 'done', value: false }], orderBy: 'updatedAt' }) // field names are checked against Note
await notes.put({ text: 1, done: false }) // compile error

Interfaces and type aliases both work. A collection without a type argument accepts any JSON object. Failed calls throw a DgError whose code is typed, so e.code === 'quota_exceeded' autocompletes.

See and manage your app's data and users

Everything your app stores, and everyone who connected it, is in one place: My apps → Data and users (open it from your app's card). Only you, the app's owner, can open it. A dg.run administrator can also open it to help with a support case, and what an administrator opens, edits or deletes there is recorded in an audit trail.

The page shows the live app only. Admin previews use separate data that never appears here. Until your app is live and someone has connected, it says so. The header counts connected users, documents and active subscribers, and it has three tabs: Data, Users and Access.

Data

Every collection your app uses, with its kind (private or public) and its document count, private ones included. Pick a collection to filter the list, or choose All documents. Each row shows the document's key, its collection, its owner (their email address), when it last changed and its size. Lists show 25 documents per page, newest change first.

  • Edit opens the document's JSON. Saving follows the same format rules as the SDK: a JSON object, at most 16 KB, no field names that start with $ or contain .. In a collection with record permissions, you can also edit the document's own acl lists (who may read, update and delete it).
  • Delete removes the document for good (there is no undo).
  • Your edits and deletions reach pages that are watching the collection at once (see Realtime).
  • You are the owner, so edits made here are not blocked by the collection's rules or by protected fields. Only the format and size limits apply.

Users

Everyone who connected your app, newest first. For each person you see:

Column What it is
Email The address they shared with your app when they allowed it. A blocked badge means dg.run blocked the account: their calls fail.
ID in the app Their dg.user.id, and the owner of every document they create. It is different in every other app, so you can match what you see here with what your code sees.
Connected When they first allowed your app.
Documents How many documents they own in your app.
Plan, time left, auto-renew What they hold if you sell plans: the plan's name, the days left (or Lifetime), and whether it renews. Opening the list renews or ends whatever is due, so it is never out of date.
Permissions Their permission tags, or "app default".

For each person you can:

  • View data: list only their documents (and edit or delete them like any other).
  • Edit permissions: give them their own list of tags, or take them back to the app's default tags. It reaches them on their next request.
  • Disconnect user: ends their session at once and erases everything they stored in your app. Their next call fails with unauthorized and the SDK signs them out on their device. They can connect again later, starting empty. This cannot be undone.

Access

The default permission tags and the rules of each collection: who may read, create, update and delete, protected fields and record permissions. It is described in Roles and permissions, and a change takes effect on the next request.

Matching a person with their data

A user reports a problem and tells you their email address. Open Users, find them and tap View data to see exactly what they see. In your own code, the same person is dg.user.id:

// The value in "ID in the app", and the `owner` of each of their documents:
const mine = (await notes.list({ limit: 100 })).items // their own private documents
console.log(dg.user.id === mine[0]?.owner)            // true

// Public collections: tell the author's documents from the others'
const guestbook = dg.db.collection<{ text: string }>('guestbook', 'public')
const { items } = await guestbook.list({ orderBy: 'createdAt', desc: true })
const own = items.filter((d) => d.owner === dg.user?.id)

To let someone moderate (delete other people's messages, say), give them a tag such as moderator under Users, add it to the collection's Delete rule under Access, and show the button only to people who hold it:

const moderator = !!dg.user?.permissions.includes('moderator') // decides what to SHOW; the server still checks every call
await guestbook.delete(doc.key, { owner: doc.owner })          // someone else's message; 'forbidden' unless the rules allow it

Good to know

  • Users are told on the consent screen that you can see what your app stores for them, and that your app receives their email address. Store only what you need.
  • There is no export or API-key access yet: everything on this page is done in the dashboard.
  • The page needs no code in your app and nothing to switch on.

Things to know

  • Sign-in lasts 30 days on a device and renews itself while the user keeps using the app. The SDK keeps a 15-minute access token in memory only; the long-lived part is an httpOnly cookie your code can't read. await dg.ready before reading dg.user on page load. After 30 idle days, signIn() brings the user back in one tap. signOut() also ends the session on the server.
  • Realtime. collection.live and collection.subscribe push changes to your page; without a connection, live lists again every 15 seconds. See Realtime.
  • Offline. Calls fail without a connection. If your app works offline (see installing and offline use), keep a local copy of what the user needs and don't cache anything under /_dg/ in your service worker.
  • _dg/ is reserved. A bundle with a top-level _dg folder is rejected.
  • Admin previews use their own, separate data, so reviewing a version never touches your live users' data.
  • Privacy. Your app receives the user's email address and nothing else about their account. As the developer you can see, edit and delete everything your app stores, private data included, so store only what you need and tell your users what you keep. Users can disconnect an app and erase its data about them.

Want to charge for your app? See Plans and subscriptions.

Realtime

collection.subscribe and collection.live tell your page when documents change, without polling. Only changes the signed-in user is allowed to read are sent (the same rules as get and list).

const notes = dg.db.collection<Note>('notes', 'public')

// Keep a list on screen: called now, after every change, and after a reconnect.
const stop = notes.live({ orderBy: 'createdAt', desc: true, limit: 20 }, (page) => render(page.items))

// Or react to each change yourself.
const off = notes.subscribe((e) => {
  if (e.type === 'put') upsert(e.doc)            // created or changed
  else if (e.type === 'delete') remove(e.key)    // removed
  else list()                                    // 'reset': connected again, you may have missed something
})

The connection opens on your first subscription and closes shortly after the last one stops. Connections are renewed about every five minutes; the SDK reconnects by itself and sends reset so you can list again. If a connection cannot be made, live falls back to listing every 15 seconds, and dg.realtime.status ('open' | 'connecting' | 'closed' | 'off', with dg.realtime.onStatus(fn)) tells you which mode you are in. At most 20 collections can be watched at once. Pass realtime: false to createDg to turn it off.

Samples

Complete apps you can build, zip and upload (each folder has a README with the steps, and a prebuilt .zip):

  • basic-example: React islands on plain HTML pages in English and French. A public guestbook and private notes, TypeScript types, collection.live with a Live badge, and a moderator role that can delete anyone's message (configured in Data and users → Access / Users).
  • pro-notes: a free tier, a free trial, monthly and yearly plans and a one-time purchase, with dg.billing.subscribe() and entitlement() (see Plans and subscriptions).
  • market-pwa: a counter with two different pages on one address, for the marketplace listing and for the installed app.
  • todo-pwa: a small installable Vite + React app with a service worker, as a starting point for installing and offline use (no sign-in or database).

basic-example and pro-notes import from @dgrun/sdk. In your own project, run npm install @dgrun/sdk and bundle it with Vite, esbuild or any bundler.