Browse the docs

Examples

Signature example

Share an agreement or PDF and collect drawn signatures tied to verified identities. Storage, summary counters, and email.

bunx @revnu/fuast new nda --template signature

Share an agreement or PDF with your team and collect signatures. Each signature is tied to the signer's verified fuast account.

What it shows

  • auth: { requireSignIn: true }: no guests, because a signature needs a real identity.
  • documents and signatures tables linked by docId: id("documents"), with a by_doc_signer index that enforces one signature per person.
  • Attachments and signature images uploaded with client.storage.upload. Rows store only the storage key, and the document query turns keys into URLs with ctx.storage.url().
  • Client-sent keys checked with ctx.storage.stat() before a row points at them: the object must exist and the caller must have uploaded it, so nobody can attach (and later delete) someone else's file.
  • A signatureCount field updated in the same transaction as each signature, so the list never counts rows.
  • An email to the author for every new signature, sent as the last step of the mutation.
  • deleteDocument removes rows first and storage objects last, because storage is outside the transaction.
  • Client routes / and /docs/:id, and a dependency-free canvas signature pad.

Upload, then write

const uploaded = await client.storage.upload(png);   // private by default
await sign(docId, uploaded.key);
async function uploadedBy(ctx: { storage: StorageContext }, key: string, userId: string) {
  if (!isStorageKey(key)) return false;
  const info = await ctx.storage.stat(key);           // null when the object does not exist
  return info !== null && info.uploadedBy === userId;
}

sign: mutation(async (ctx, docId: string, imageKey: string) => {
  const me = ctx.auth.requireSignedIn();
  if (!(await uploadedBy(ctx, imageKey, me.userId))) throw new Error("Draw your signature again.");
  const doc = await ctx.db.documents.get(docId);
  if (!doc) throw new Error("This document no longer exists.");
  const existing = await ctx.db.signatures
    .withIndex("by_doc_signer", (q) => q.eq("docId", docId).eq("signerId", me.userId))
    .first();
  if (existing) throw new Error("You already signed this document.");
  await ctx.db.signatures.insert({ docId, signerId: me.userId, name: me.displayName, imageKey });
  await ctx.db.documents.update(docId, { signatureCount: doc.signatureCount + 1 });
  // email the author last
})

The signer's name and email come from ctx.auth, so nobody can sign as someone else. createDocument checks the attachment key the same way.

Try it

bunx @revnu/fuast dev

Create a document as dev, then open it as ?fuast_user=alice and sign. The author's email appears in bunx @revnu/fuast logs --port 3000. Deploy and share with your workspace:

bunx @revnu/fuast deploy
bunx @revnu/fuast share --visibility workspace

Add an outside signer with --emails them@partner.com.