@proveanything/smartlinks 2.0.0-alpha.2 → 2.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/sequence.d.ts +29 -0
- package/dist/api/sequence.js +26 -0
- package/dist/docs/API_SUMMARY.md +28 -1
- package/dist/docs/sequences.md +108 -0
- package/dist/openapi.yaml +25 -0
- package/docs/API_SUMMARY.md +28 -1
- package/docs/sequences.md +108 -0
- package/openapi.yaml +25 -0
- package/package.json +1 -1
package/dist/api/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { auth } from "./auth";
|
|
|
10
10
|
export { form } from "./form";
|
|
11
11
|
export { authKit } from "./authKit";
|
|
12
12
|
export { claimSet } from "./claimSet";
|
|
13
|
+
export { sequence } from "./sequence";
|
|
13
14
|
export { crate } from "./crate";
|
|
14
15
|
export { batch } from "./batch";
|
|
15
16
|
export { variant } from "./variant";
|
package/dist/api/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { auth } from "./auth";
|
|
|
12
12
|
export { form } from "./form";
|
|
13
13
|
export { authKit } from "./authKit";
|
|
14
14
|
export { claimSet } from "./claimSet";
|
|
15
|
+
export { sequence } from "./sequence";
|
|
15
16
|
export { crate } from "./crate";
|
|
16
17
|
export { batch } from "./batch";
|
|
17
18
|
export { variant } from "./variant";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface AllocateSequenceInput {
|
|
2
|
+
/** The app that owns the sequence config. */
|
|
3
|
+
appId: string;
|
|
4
|
+
/** The configured sequence id — the key of data.sequenceConfigs on the app config. */
|
|
5
|
+
sequenceId: string;
|
|
6
|
+
/** The STABLE subject identity — the claim-set id from the tap (NOT a per-tap virtual id). */
|
|
7
|
+
subjectId: string;
|
|
8
|
+
/** Optional product scope, when the sequence config is scoped per product. */
|
|
9
|
+
productId?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AllocatedSequence {
|
|
12
|
+
/** The allocated (or already-held) number. */
|
|
13
|
+
number: number;
|
|
14
|
+
/** True when this call allocated a new number; false when the subject already had one. */
|
|
15
|
+
isNew: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare namespace sequence {
|
|
18
|
+
/**
|
|
19
|
+
* Allocate (or return the existing) sequence number for a subject. Idempotent — safe to
|
|
20
|
+
* call on load (auto-enter) and on a button tap; a subject that already has a number gets
|
|
21
|
+
* it back with `isNew: false`.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const { number, isNew } = await sequence.allocate(collectionId, {
|
|
25
|
+
* appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId,
|
|
26
|
+
* })
|
|
27
|
+
*/
|
|
28
|
+
function allocate(collectionId: string, input: AllocateSequenceInput): Promise<AllocatedSequence>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/api/sequence.ts
|
|
2
|
+
//
|
|
3
|
+
// Sequences — allocate a guaranteed-unique, monotonic number (raffle tickets, "Nth to
|
|
4
|
+
// claim", queue positions) and stamp it onto a record. The sequence (counter key, target,
|
|
5
|
+
// field, scope) is configured server-side in app config (data.sequenceConfigs[sequenceId]);
|
|
6
|
+
// this call supplies only the subject and is idempotent — a re-tap returns the same number.
|
|
7
|
+
// See docs/sequences.md.
|
|
8
|
+
import { post } from "../http";
|
|
9
|
+
export var sequence;
|
|
10
|
+
(function (sequence) {
|
|
11
|
+
const base = (collectionId) => `/public/collection/${encodeURIComponent(collectionId)}/sequence`;
|
|
12
|
+
/**
|
|
13
|
+
* Allocate (or return the existing) sequence number for a subject. Idempotent — safe to
|
|
14
|
+
* call on load (auto-enter) and on a button tap; a subject that already has a number gets
|
|
15
|
+
* it back with `isNew: false`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const { number, isNew } = await sequence.allocate(collectionId, {
|
|
19
|
+
* appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId,
|
|
20
|
+
* })
|
|
21
|
+
*/
|
|
22
|
+
async function allocate(collectionId, input) {
|
|
23
|
+
return post(`${base(collectionId)}/allocate`, input);
|
|
24
|
+
}
|
|
25
|
+
sequence.allocate = allocate;
|
|
26
|
+
})(sequence || (sequence = {}));
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.0-alpha.
|
|
3
|
+
Version: 2.0.0-alpha.4 | Generated: 2026-09-14T15:08:56.614Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -54,6 +54,7 @@ For detailed guides on specific features:
|
|
|
54
54
|
- **[Forms](forms.md)** - Platform-managed form definitions, submissions, and schema-driven React form UI
|
|
55
55
|
- **[App Objects: Cases, Threads & Records](app-objects.md)** - Generic app-scoped building blocks for support cases, discussions, bookings, registrations, and more
|
|
56
56
|
- **[App Records Pattern](app-records-pattern.md)** - Canonical pattern for storing per-product, per-facet, or rule-targeted app data
|
|
57
|
+
- **[Sequences & Claim-Order](sequences.md)** - Allocate a guaranteed-unique, monotonic number (raffle tickets, "Nth to claim", queue positions) and stamp it onto a record — atomic app-config counter + idempotent per-subject stamping, concurrency-safe at stadium scale
|
|
57
58
|
- **[Communications](comms.md)** - Transactional sends, multi-channel broadcasts, consent management, push registration, and analytics
|
|
58
59
|
- **[Interactions & Event Tracking](interactions.md)** - Log user events, count outcomes, query history, and define interaction types with permissions
|
|
59
60
|
- **[Analytics](analytics.md)** - Web analytics, link-click tracking, QR/tag scan telemetry, and event reporting
|
|
@@ -149,6 +150,7 @@ The Smartlinks SDK is organized into the following namespaces:
|
|
|
149
150
|
- **realtime** - Functions for realtime operations
|
|
150
151
|
- **research** - Functions for research operations
|
|
151
152
|
- **secrets** - Functions for secrets operations
|
|
153
|
+
- **sequence** - Functions for sequence operations
|
|
152
154
|
- **tags** - Functions for tags operations
|
|
153
155
|
- **template** - Functions for template operations
|
|
154
156
|
- **translations** - Functions for translations operations
|
|
@@ -8534,6 +8536,26 @@ type VerifyTokenResponse = {
|
|
|
8534
8536
|
}
|
|
8535
8537
|
```
|
|
8536
8538
|
|
|
8539
|
+
### sequence (api)
|
|
8540
|
+
|
|
8541
|
+
**AllocateSequenceInput** (interface)
|
|
8542
|
+
```typescript
|
|
8543
|
+
interface AllocateSequenceInput {
|
|
8544
|
+
appId: string
|
|
8545
|
+
sequenceId: string
|
|
8546
|
+
subjectId: string
|
|
8547
|
+
productId?: string
|
|
8548
|
+
}
|
|
8549
|
+
```
|
|
8550
|
+
|
|
8551
|
+
**AllocatedSequence** (interface)
|
|
8552
|
+
```typescript
|
|
8553
|
+
interface AllocatedSequence {
|
|
8554
|
+
number: number
|
|
8555
|
+
isNew: boolean
|
|
8556
|
+
}
|
|
8557
|
+
```
|
|
8558
|
+
|
|
8537
8559
|
### conditions (utils)
|
|
8538
8560
|
|
|
8539
8561
|
**BaseCondition** (interface)
|
|
@@ -10790,6 +10812,11 @@ Soft-delete a secret. DELETE /secrets/:ref
|
|
|
10790
10812
|
id: string,
|
|
10791
10813
|
query: { limit?: number; offset?: number } = {}) → `Promise<SegmentRecipientsResponse>`
|
|
10792
10814
|
|
|
10815
|
+
### sequence
|
|
10816
|
+
|
|
10817
|
+
**allocate**(collectionId: string, input: AllocateSequenceInput) → `Promise<AllocatedSequence>`
|
|
10818
|
+
Allocate (or return the existing) sequence number for a subject. Idempotent — safe to call on load (auto-enter) and on a button tap; a subject that already has a number gets it back with `isNew: false`. const { number, isNew } = await sequence.allocate(collectionId, { appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId, })
|
|
10819
|
+
|
|
10793
10820
|
### sessions
|
|
10794
10821
|
|
|
10795
10822
|
**stats**(collectionId: string) → `Promise<SessionStatistics>`
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Sequences & claim-order allocation
|
|
2
|
+
|
|
3
|
+
> **Preview — SmartLinks SDK 2.0.0-alpha.** APIs may change before 2.0.0 stable.
|
|
4
|
+
|
|
5
|
+
A **sequence** hands out a guaranteed-unique, monotonic number — `1, 2, 3, …` — and stamps it
|
|
6
|
+
onto a record. It's the primitive behind raffle tickets, "you're the Nth to claim", queue
|
|
7
|
+
positions, and limited-edition numbering: cases where you need *the next number*, exactly
|
|
8
|
+
once per subject, even with a whole room (or stadium) acting at the same instant.
|
|
9
|
+
|
|
10
|
+
## The model: allocate, then stamp — and the target is the ledger
|
|
11
|
+
|
|
12
|
+
Two steps, one call:
|
|
13
|
+
|
|
14
|
+
1. **Allocate** — get the next number from an **atomic counter**. The counter lives inside
|
|
15
|
+
your **app-config** doc at `data.sequences.<key>`, so it's scoped exactly like your other
|
|
16
|
+
app config (per collection / product / variant / batch) and needs no dedicated storage.
|
|
17
|
+
It's incremented in a single statement, so 250 simultaneous taps each get a distinct
|
|
18
|
+
number in microseconds — never two the same.
|
|
19
|
+
2. **Stamp** — write that number onto the **subject's own record** (a claim set, a proof, or
|
|
20
|
+
an app record). **That record is the ledger** — there's no separate bookkeeping table.
|
|
21
|
+
Next time you read the subject, the number is just there.
|
|
22
|
+
|
|
23
|
+
Because the target is the ledger, allocation is **idempotent**: on a re-tap we read the
|
|
24
|
+
subject first and return the number it already has, rather than allocating a second one.
|
|
25
|
+
|
|
26
|
+
## Guarantees
|
|
27
|
+
|
|
28
|
+
- **Unique + monotonic** — the counter is an atomic increment, never a read-modify-write, so
|
|
29
|
+
there are no duplicates and no lost updates under load.
|
|
30
|
+
- **Idempotent per subject** — one number per subject; a re-tap returns the same number.
|
|
31
|
+
- **Concurrency-safe at scale** — the only shared hot spot is the one counter row (Postgres
|
|
32
|
+
serializes it); the stamp is per-subject (its own record), so the model holds from 250 in a
|
|
33
|
+
room to 60,000 in a stadium. A concurrent double-tap on the *same* subject at worst skips
|
|
34
|
+
one number (a harmless gap) — never puts two numbers on one person.
|
|
35
|
+
|
|
36
|
+
## Scoping
|
|
37
|
+
|
|
38
|
+
- **Counter scope** — the app-config doc it lives on: collection-wide, or per product /
|
|
39
|
+
variant / batch. All wristbands mapped to one product? Scope the counter to that product.
|
|
40
|
+
- **`key`** — a name within that doc (e.g. `raffle:2026-cup`), so one config doc can hold
|
|
41
|
+
several independent sequences.
|
|
42
|
+
- **`start`** — the first number (default `1`); use it to reserve a block (start at `51` and
|
|
43
|
+
tell people it's `1`, or just filter `< N` at draw time).
|
|
44
|
+
|
|
45
|
+
## Idempotency subject — use a *stable* id
|
|
46
|
+
|
|
47
|
+
The number is deduped by the **subject id** you pass, so it must be the **stable** identity:
|
|
48
|
+
- **Claim set id** (the wristband's permanent record) — the right key when people aren't
|
|
49
|
+
logged in. A re-tap resolves to the same claim set → same number.
|
|
50
|
+
- The authenticated **user/contact** — if they sign in / claim.
|
|
51
|
+
|
|
52
|
+
Do **not** key on a value that changes per interaction (e.g. a virtual proof id minted fresh
|
|
53
|
+
on each tap) — that would let one person take several numbers.
|
|
54
|
+
|
|
55
|
+
## Where the number is stored (the sink)
|
|
56
|
+
|
|
57
|
+
The stamp target is configurable — the number lives wherever you'll read it:
|
|
58
|
+
|
|
59
|
+
| Target | Use |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `claimSet` | The Firestore wristband record — fast, no proof mint on the hot path. |
|
|
62
|
+
| `proof` | A minted proof (value or attestation) — when the number should travel with the proof. |
|
|
63
|
+
| `appRecord` | A structured app record — for queryable, per-app data. |
|
|
64
|
+
|
|
65
|
+
## Configure the sequence (once, server-side)
|
|
66
|
+
|
|
67
|
+
You define the sequence **in your app config**, under `data.sequenceConfigs`. This is what
|
|
68
|
+
makes the public endpoint safe: the target/field/scope are set by you, not the caller.
|
|
69
|
+
|
|
70
|
+
```jsonc
|
|
71
|
+
// app config: data.sequenceConfigs
|
|
72
|
+
{
|
|
73
|
+
"raffle": {
|
|
74
|
+
"key": "raffle:2026-cup", // the named counter (data.sequences.<key>)
|
|
75
|
+
"target": "claimSet", // claimSet | proof | appRecord — where the number is stamped
|
|
76
|
+
"field": "raffleNumber", // the property written on the target
|
|
77
|
+
"productId": "wristbands-2026", // counter scope (optional; else collection-wide)
|
|
78
|
+
"start": 1 // first number (optional, default 1)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Call it (the widget)
|
|
84
|
+
|
|
85
|
+
Your widget calls one bounded, public endpoint on tap. It passes only the **subject id** —
|
|
86
|
+
never the target/field:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
POST /api/v1/public/collection/:collectionId/sequence/allocate
|
|
90
|
+
{ "appId": "raffle-app", "sequenceId": "raffle", "subjectId": "<claim set id>" }
|
|
91
|
+
→ { "number": 42, "isNew": true } // re-tap → { "number": 42, "isNew": false }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- **`subjectId`** is the STABLE identity — the claim-set id from the tap. Re-taps collapse to
|
|
95
|
+
one number.
|
|
96
|
+
- **Idempotent**, so both your flows are the *same call*:
|
|
97
|
+
- **Auto:** on load, call allocate → get your number (existing or freshly minted).
|
|
98
|
+
- **Button:** click → animate → same call → "Your raffle number is 42."
|
|
99
|
+
- **Refresh:** just read the `raffleNumber` field back off the claim set (or proof) — it's the
|
|
100
|
+
ledger. Or call allocate again; you'll get the same number with `isNew: false`.
|
|
101
|
+
|
|
102
|
+
Errors: `404 SEQUENCE_NOT_FOUND` (not configured), `404 CLAIMSET_NOT_FOUND` (bad subject),
|
|
103
|
+
`400 BAD_REQUEST` (missing fields).
|
|
104
|
+
|
|
105
|
+
## Drawing the winner
|
|
106
|
+
|
|
107
|
+
No separate ledger — query the claim sets (or proofs) where `raffleNumber` is set; that field
|
|
108
|
+
is your entry list, in allocation order.
|
package/dist/openapi.yaml
CHANGED
|
@@ -27897,3 +27897,28 @@ components:
|
|
|
27897
27897
|
additionalProperties: true
|
|
27898
27898
|
required:
|
|
27899
27899
|
- valid
|
|
27900
|
+
AllocateSequenceInput:
|
|
27901
|
+
type: object
|
|
27902
|
+
properties:
|
|
27903
|
+
appId:
|
|
27904
|
+
type: string
|
|
27905
|
+
sequenceId:
|
|
27906
|
+
type: string
|
|
27907
|
+
subjectId:
|
|
27908
|
+
type: string
|
|
27909
|
+
productId:
|
|
27910
|
+
type: string
|
|
27911
|
+
required:
|
|
27912
|
+
- appId
|
|
27913
|
+
- sequenceId
|
|
27914
|
+
- subjectId
|
|
27915
|
+
AllocatedSequence:
|
|
27916
|
+
type: object
|
|
27917
|
+
properties:
|
|
27918
|
+
number:
|
|
27919
|
+
type: number
|
|
27920
|
+
isNew:
|
|
27921
|
+
type: boolean
|
|
27922
|
+
required:
|
|
27923
|
+
- number
|
|
27924
|
+
- isNew
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.0-alpha.
|
|
3
|
+
Version: 2.0.0-alpha.4 | Generated: 2026-09-14T15:08:56.614Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -54,6 +54,7 @@ For detailed guides on specific features:
|
|
|
54
54
|
- **[Forms](forms.md)** - Platform-managed form definitions, submissions, and schema-driven React form UI
|
|
55
55
|
- **[App Objects: Cases, Threads & Records](app-objects.md)** - Generic app-scoped building blocks for support cases, discussions, bookings, registrations, and more
|
|
56
56
|
- **[App Records Pattern](app-records-pattern.md)** - Canonical pattern for storing per-product, per-facet, or rule-targeted app data
|
|
57
|
+
- **[Sequences & Claim-Order](sequences.md)** - Allocate a guaranteed-unique, monotonic number (raffle tickets, "Nth to claim", queue positions) and stamp it onto a record — atomic app-config counter + idempotent per-subject stamping, concurrency-safe at stadium scale
|
|
57
58
|
- **[Communications](comms.md)** - Transactional sends, multi-channel broadcasts, consent management, push registration, and analytics
|
|
58
59
|
- **[Interactions & Event Tracking](interactions.md)** - Log user events, count outcomes, query history, and define interaction types with permissions
|
|
59
60
|
- **[Analytics](analytics.md)** - Web analytics, link-click tracking, QR/tag scan telemetry, and event reporting
|
|
@@ -149,6 +150,7 @@ The Smartlinks SDK is organized into the following namespaces:
|
|
|
149
150
|
- **realtime** - Functions for realtime operations
|
|
150
151
|
- **research** - Functions for research operations
|
|
151
152
|
- **secrets** - Functions for secrets operations
|
|
153
|
+
- **sequence** - Functions for sequence operations
|
|
152
154
|
- **tags** - Functions for tags operations
|
|
153
155
|
- **template** - Functions for template operations
|
|
154
156
|
- **translations** - Functions for translations operations
|
|
@@ -8534,6 +8536,26 @@ type VerifyTokenResponse = {
|
|
|
8534
8536
|
}
|
|
8535
8537
|
```
|
|
8536
8538
|
|
|
8539
|
+
### sequence (api)
|
|
8540
|
+
|
|
8541
|
+
**AllocateSequenceInput** (interface)
|
|
8542
|
+
```typescript
|
|
8543
|
+
interface AllocateSequenceInput {
|
|
8544
|
+
appId: string
|
|
8545
|
+
sequenceId: string
|
|
8546
|
+
subjectId: string
|
|
8547
|
+
productId?: string
|
|
8548
|
+
}
|
|
8549
|
+
```
|
|
8550
|
+
|
|
8551
|
+
**AllocatedSequence** (interface)
|
|
8552
|
+
```typescript
|
|
8553
|
+
interface AllocatedSequence {
|
|
8554
|
+
number: number
|
|
8555
|
+
isNew: boolean
|
|
8556
|
+
}
|
|
8557
|
+
```
|
|
8558
|
+
|
|
8537
8559
|
### conditions (utils)
|
|
8538
8560
|
|
|
8539
8561
|
**BaseCondition** (interface)
|
|
@@ -10790,6 +10812,11 @@ Soft-delete a secret. DELETE /secrets/:ref
|
|
|
10790
10812
|
id: string,
|
|
10791
10813
|
query: { limit?: number; offset?: number } = {}) → `Promise<SegmentRecipientsResponse>`
|
|
10792
10814
|
|
|
10815
|
+
### sequence
|
|
10816
|
+
|
|
10817
|
+
**allocate**(collectionId: string, input: AllocateSequenceInput) → `Promise<AllocatedSequence>`
|
|
10818
|
+
Allocate (or return the existing) sequence number for a subject. Idempotent — safe to call on load (auto-enter) and on a button tap; a subject that already has a number gets it back with `isNew: false`. const { number, isNew } = await sequence.allocate(collectionId, { appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId, })
|
|
10819
|
+
|
|
10793
10820
|
### sessions
|
|
10794
10821
|
|
|
10795
10822
|
**stats**(collectionId: string) → `Promise<SessionStatistics>`
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Sequences & claim-order allocation
|
|
2
|
+
|
|
3
|
+
> **Preview — SmartLinks SDK 2.0.0-alpha.** APIs may change before 2.0.0 stable.
|
|
4
|
+
|
|
5
|
+
A **sequence** hands out a guaranteed-unique, monotonic number — `1, 2, 3, …` — and stamps it
|
|
6
|
+
onto a record. It's the primitive behind raffle tickets, "you're the Nth to claim", queue
|
|
7
|
+
positions, and limited-edition numbering: cases where you need *the next number*, exactly
|
|
8
|
+
once per subject, even with a whole room (or stadium) acting at the same instant.
|
|
9
|
+
|
|
10
|
+
## The model: allocate, then stamp — and the target is the ledger
|
|
11
|
+
|
|
12
|
+
Two steps, one call:
|
|
13
|
+
|
|
14
|
+
1. **Allocate** — get the next number from an **atomic counter**. The counter lives inside
|
|
15
|
+
your **app-config** doc at `data.sequences.<key>`, so it's scoped exactly like your other
|
|
16
|
+
app config (per collection / product / variant / batch) and needs no dedicated storage.
|
|
17
|
+
It's incremented in a single statement, so 250 simultaneous taps each get a distinct
|
|
18
|
+
number in microseconds — never two the same.
|
|
19
|
+
2. **Stamp** — write that number onto the **subject's own record** (a claim set, a proof, or
|
|
20
|
+
an app record). **That record is the ledger** — there's no separate bookkeeping table.
|
|
21
|
+
Next time you read the subject, the number is just there.
|
|
22
|
+
|
|
23
|
+
Because the target is the ledger, allocation is **idempotent**: on a re-tap we read the
|
|
24
|
+
subject first and return the number it already has, rather than allocating a second one.
|
|
25
|
+
|
|
26
|
+
## Guarantees
|
|
27
|
+
|
|
28
|
+
- **Unique + monotonic** — the counter is an atomic increment, never a read-modify-write, so
|
|
29
|
+
there are no duplicates and no lost updates under load.
|
|
30
|
+
- **Idempotent per subject** — one number per subject; a re-tap returns the same number.
|
|
31
|
+
- **Concurrency-safe at scale** — the only shared hot spot is the one counter row (Postgres
|
|
32
|
+
serializes it); the stamp is per-subject (its own record), so the model holds from 250 in a
|
|
33
|
+
room to 60,000 in a stadium. A concurrent double-tap on the *same* subject at worst skips
|
|
34
|
+
one number (a harmless gap) — never puts two numbers on one person.
|
|
35
|
+
|
|
36
|
+
## Scoping
|
|
37
|
+
|
|
38
|
+
- **Counter scope** — the app-config doc it lives on: collection-wide, or per product /
|
|
39
|
+
variant / batch. All wristbands mapped to one product? Scope the counter to that product.
|
|
40
|
+
- **`key`** — a name within that doc (e.g. `raffle:2026-cup`), so one config doc can hold
|
|
41
|
+
several independent sequences.
|
|
42
|
+
- **`start`** — the first number (default `1`); use it to reserve a block (start at `51` and
|
|
43
|
+
tell people it's `1`, or just filter `< N` at draw time).
|
|
44
|
+
|
|
45
|
+
## Idempotency subject — use a *stable* id
|
|
46
|
+
|
|
47
|
+
The number is deduped by the **subject id** you pass, so it must be the **stable** identity:
|
|
48
|
+
- **Claim set id** (the wristband's permanent record) — the right key when people aren't
|
|
49
|
+
logged in. A re-tap resolves to the same claim set → same number.
|
|
50
|
+
- The authenticated **user/contact** — if they sign in / claim.
|
|
51
|
+
|
|
52
|
+
Do **not** key on a value that changes per interaction (e.g. a virtual proof id minted fresh
|
|
53
|
+
on each tap) — that would let one person take several numbers.
|
|
54
|
+
|
|
55
|
+
## Where the number is stored (the sink)
|
|
56
|
+
|
|
57
|
+
The stamp target is configurable — the number lives wherever you'll read it:
|
|
58
|
+
|
|
59
|
+
| Target | Use |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `claimSet` | The Firestore wristband record — fast, no proof mint on the hot path. |
|
|
62
|
+
| `proof` | A minted proof (value or attestation) — when the number should travel with the proof. |
|
|
63
|
+
| `appRecord` | A structured app record — for queryable, per-app data. |
|
|
64
|
+
|
|
65
|
+
## Configure the sequence (once, server-side)
|
|
66
|
+
|
|
67
|
+
You define the sequence **in your app config**, under `data.sequenceConfigs`. This is what
|
|
68
|
+
makes the public endpoint safe: the target/field/scope are set by you, not the caller.
|
|
69
|
+
|
|
70
|
+
```jsonc
|
|
71
|
+
// app config: data.sequenceConfigs
|
|
72
|
+
{
|
|
73
|
+
"raffle": {
|
|
74
|
+
"key": "raffle:2026-cup", // the named counter (data.sequences.<key>)
|
|
75
|
+
"target": "claimSet", // claimSet | proof | appRecord — where the number is stamped
|
|
76
|
+
"field": "raffleNumber", // the property written on the target
|
|
77
|
+
"productId": "wristbands-2026", // counter scope (optional; else collection-wide)
|
|
78
|
+
"start": 1 // first number (optional, default 1)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Call it (the widget)
|
|
84
|
+
|
|
85
|
+
Your widget calls one bounded, public endpoint on tap. It passes only the **subject id** —
|
|
86
|
+
never the target/field:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
POST /api/v1/public/collection/:collectionId/sequence/allocate
|
|
90
|
+
{ "appId": "raffle-app", "sequenceId": "raffle", "subjectId": "<claim set id>" }
|
|
91
|
+
→ { "number": 42, "isNew": true } // re-tap → { "number": 42, "isNew": false }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- **`subjectId`** is the STABLE identity — the claim-set id from the tap. Re-taps collapse to
|
|
95
|
+
one number.
|
|
96
|
+
- **Idempotent**, so both your flows are the *same call*:
|
|
97
|
+
- **Auto:** on load, call allocate → get your number (existing or freshly minted).
|
|
98
|
+
- **Button:** click → animate → same call → "Your raffle number is 42."
|
|
99
|
+
- **Refresh:** just read the `raffleNumber` field back off the claim set (or proof) — it's the
|
|
100
|
+
ledger. Or call allocate again; you'll get the same number with `isNew: false`.
|
|
101
|
+
|
|
102
|
+
Errors: `404 SEQUENCE_NOT_FOUND` (not configured), `404 CLAIMSET_NOT_FOUND` (bad subject),
|
|
103
|
+
`400 BAD_REQUEST` (missing fields).
|
|
104
|
+
|
|
105
|
+
## Drawing the winner
|
|
106
|
+
|
|
107
|
+
No separate ledger — query the claim sets (or proofs) where `raffleNumber` is set; that field
|
|
108
|
+
is your entry list, in allocation order.
|
package/openapi.yaml
CHANGED
|
@@ -27897,3 +27897,28 @@ components:
|
|
|
27897
27897
|
additionalProperties: true
|
|
27898
27898
|
required:
|
|
27899
27899
|
- valid
|
|
27900
|
+
AllocateSequenceInput:
|
|
27901
|
+
type: object
|
|
27902
|
+
properties:
|
|
27903
|
+
appId:
|
|
27904
|
+
type: string
|
|
27905
|
+
sequenceId:
|
|
27906
|
+
type: string
|
|
27907
|
+
subjectId:
|
|
27908
|
+
type: string
|
|
27909
|
+
productId:
|
|
27910
|
+
type: string
|
|
27911
|
+
required:
|
|
27912
|
+
- appId
|
|
27913
|
+
- sequenceId
|
|
27914
|
+
- subjectId
|
|
27915
|
+
AllocatedSequence:
|
|
27916
|
+
type: object
|
|
27917
|
+
properties:
|
|
27918
|
+
number:
|
|
27919
|
+
type: number
|
|
27920
|
+
isNew:
|
|
27921
|
+
type: boolean
|
|
27922
|
+
required:
|
|
27923
|
+
- number
|
|
27924
|
+
- isNew
|