Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/four-chefs-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@learncard/types": patch
"@learncard/network-plugin": patch
"@learncard/network-brain-service": patch
"@workspace/e2e-tests": patch
---

feat: [LC-1103] Dynamic Boost Templates
149 changes: 149 additions & 0 deletions docs/core-concepts/credentials-and-data/boost-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,155 @@ const query = {

The query system is powered by a Neo4j database and includes helper functions for converting JavaScript query objects to Neo4j-compatible formats.

## Dynamic Templates with Mustache

Boosts support **dynamic templating** using Mustache-style variables (`{{variableName}}`). This allows you to create reusable credential templates where specific values are injected at the time of issuance.

### Why Use Dynamic Templates?

Dynamic templates solve a common challenge: you want to issue credentials that share the same structure but contain personalized data for each recipient. Instead of creating a new Boost for each variation, you create one template and provide the unique data when sending.

**Use cases include:**
- **Personalized certificates**: Include the recipient's name, completion date, or score
- **Course completions**: Dynamic course name, grade, or instructor
- **Event attendance**: Event date, location, or session details
- **Achievement levels**: Tier, points earned, or ranking

### Creating a Templated Boost

Define your Boost credential with Mustache variables where you want dynamic values:

```typescript
const templatedCredential = {
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json',
'https://ctx.learncard.com/boosts/1.0.3.json',
],
type: ['VerifiableCredential', 'OpenBadgeCredential', 'BoostCredential'],
issuer: 'did:web:example.com',
name: 'Certificate for {{courseName}}',
credentialSubject: {
id: 'did:example:recipient',
type: ['AchievementSubject'],
achievement: {
id: 'urn:uuid:123',
type: ['Achievement'],
achievementType: 'Course',
name: '{{courseName}} - {{level}}',
description: 'Awarded to {{studentName}} for completing {{courseName}}',
criteria: {
narrative: 'Complete the {{courseName}} course with grade {{grade}}.',
},
},
},
};

// Create the boost template
const boostUri = await learnCard.invoke.createBoost(templatedCredential, {
name: 'Course Completion Certificate',
category: 'Education',
});
```

### Sending with Template Data

When sending the Boost, provide the `templateData` object to fill in the variables:

```typescript
// Send the boost with personalized data
const result = await learnCard.invoke.send({
type: 'boost',
recipient: 'recipient-profile-id',
templateUri: boostUri,
templateData: {
courseName: 'Web Development 101',
level: 'Beginner',
studentName: 'Alice Smith',
grade: 'A',
},
});
```

The resulting credential will have all `{{variableName}}` placeholders replaced with the corresponding values from `templateData`.

### Using sendBoost with Template Data

You can also use the `sendBoost` method directly:

```typescript
const credentialUri = await learnCard.invoke.sendBoost(
'recipient-profile-id',
boostUri,
{
encrypt: true,
templateData: {
courseName: 'Advanced TypeScript',
level: 'Advanced',
studentName: 'Bob Johnson',
grade: 'A+',
},
}
);
```

### Template Behavior

| Scenario | Behavior |
| -------- | -------- |
| Variable in template, value provided | Variable is replaced with the value |
| Variable in template, value missing | Variable is replaced with empty string |
| No variables in template | Template is used as-is (backwards compatible) |
| `templateData` provided, no variables | Data is ignored, template used as-is |

{% hint style="info" %}
**Missing Variables**: If a variable in the template is not provided in `templateData`, Mustache renders it as an empty string. This is the expected default behavior and allows for optional fields.
{% endhint %}

### Example: Event Attendance with Dynamic Date

```typescript
// Create a templated event attendance boost
const eventBoostUri = await learnCard.invoke.createBoost({
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json',
],
type: ['VerifiableCredential', 'OpenBadgeCredential', 'BoostCredential'],
issuer: 'did:web:events.example.com',
name: '{{eventName}} Attendance',
credentialSubject: {
id: 'did:example:recipient',
type: ['AchievementSubject'],
achievement: {
type: ['Achievement'],
name: '{{eventName}} - {{eventDate}}',
description: 'Attended {{eventName}} on {{eventDate}} at {{location}}',
criteria: { narrative: 'Present at the event venue' },
},
},
}, { name: 'Event Attendance Template' });

// Issue to multiple attendees with the same event data
const attendees = ['alice', 'bob', 'charlie'];
for (const profileId of attendees) {
await learnCard.invoke.send({
type: 'boost',
recipient: profileId,
templateUri: eventBoostUri,
templateData: {
eventName: 'Tech Conference 2025',
eventDate: 'January 15, 2025',
location: 'San Francisco, CA',
},
});
}
```

{% hint style="success" %}
**Backwards Compatibility**: Existing Boosts without Mustache variables continue to work exactly as before. You can safely add `templateData` to any `send` or `sendBoost` callβ€”if there are no variables in the template, the data is simply ignored.
{% endhint %}

## Types of Boosts

### Basic Boost
Expand Down
88 changes: 88 additions & 0 deletions docs/how-to-guides/send-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,94 @@ interface SendResponse {

---

## Dynamic Templates with `templateData`

Use Mustache-style templates to personalize credentials with unique data for each recipient. This is perfect for issuing the same type of credential (like course completions) with recipient-specific details.

### Creating a Templated Boost

First, create a boost with `{{variableName}}` placeholders:

```typescript
const templatedBoostUri = await learnCard.invoke.createBoost({
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json"
],
"type": ["VerifiableCredential", "OpenBadgeCredential", "BoostCredential"],
"name": "Certificate for {{courseName}}",
"credentialSubject": {
"type": ["AchievementSubject"],
"achievement": {
"type": ["Achievement"],
"name": "{{courseName}} - {{level}}",
"description": "Awarded to {{studentName}} for completing {{courseName}} with grade {{grade}}",
"criteria": { "narrative": "Complete all course modules" }
}
}
}, { name: 'Course Completion Template' });
```

### Sending with Personalized Data

Provide `templateData` when sending to substitute the variables:

```typescript
const result = await learnCard.invoke.send({
type: 'boost',
recipient: 'student-profile-id',
templateUri: templatedBoostUri,
templateData: {
courseName: 'Web Development 101',
level: 'Beginner',
studentName: 'Alice Smith',
grade: 'A',
},
});
```

The issued credential will have all placeholders replaced with the provided values.

### Batch Issuance to Multiple Recipients

```typescript
const students = [
{ profileId: 'alice', name: 'Alice Smith', grade: 'A' },
{ profileId: 'bob', name: 'Bob Johnson', grade: 'B+' },
{ profileId: 'charlie', name: 'Charlie Brown', grade: 'A-' },
];

for (const student of students) {
await learnCard.invoke.send({
type: 'boost',
recipient: student.profileId,
templateUri: templatedBoostUri,
templateData: {
courseName: 'Web Development 101',
level: 'Beginner',
studentName: student.name,
grade: student.grade,
},
});
}
```

### Special Characters

Template values are automatically escaped for JSON safety. You can safely include:
- Quotes: `"Course with \"quotes\""`
- Newlines: `"Line 1\nLine 2"`
- Backslashes: `"Path\\to\\file"`
- Unicode: `"CafΓ© β˜• ζ—₯本θͺž"`

{% hint style="info" %}
**Missing Variables**: If a variable in the template isn't provided in `templateData`, it renders as an empty string. This allows for optional fields.
{% endhint %}

For more details, see [Dynamic Templates with Mustache](../core-concepts/credentials-and-data/boost-credentials.md#dynamic-templates-with-mustache).

---

## Alternative: Universal Inbox API

Use the Universal Inbox API when you need to send credentials to **users who don't have a LearnCard profile yet**. This allows you to reach recipients via email or phone number, and they'll be guided through creating an account when they claim their credential.
Expand Down
91 changes: 90 additions & 1 deletion docs/tutorials/create-a-boost.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,12 +422,101 @@ For more details, see the [Send Credentials How-To Guide](../how-to-guides/send-

---

## Bonus: Dynamic Templates with Mustache Variables

Want to personalize credentials with unique data for each recipient? Boosts support **Mustache-style templating** that lets you inject dynamic values at issuance time.

### Creating a Templated Boost

Use `{{variableName}}` syntax in your credential template:

```typescript
const templatedCredential = {
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json',
'https://ctx.learncard.com/boosts/1.0.3.json',
],
type: ['VerifiableCredential', 'OpenBadgeCredential', 'BoostCredential'],
issuer: 'did:web:example.com',
name: 'Certificate for {{courseName}}',
credentialSubject: {
id: 'did:example:recipient',
type: ['AchievementSubject'],
achievement: {
type: ['Achievement'],
name: '{{courseName}} Completion',
description: 'Awarded to {{studentName}} for completing {{courseName}} with grade {{grade}}',
criteria: { narrative: 'Successfully complete the course' },
},
},
};

const boostUri = await learnCard.invoke.createBoost(templatedCredential, {
name: 'Course Completion Template',
});
```

### Sending with Personalized Data

Provide `templateData` when sending to fill in the variables:

```typescript
const result = await learnCard.invoke.send({
type: 'boost',
recipient: 'student-profile-id',
templateUri: boostUri,
templateData: {
courseName: 'Web Development 101',
studentName: 'Alice Smith',
grade: 'A',
},
});
```

The resulting credential will have all placeholders replaced:
- `{{courseName}}` β†’ `Web Development 101`
- `{{studentName}}` β†’ `Alice Smith`
- `{{grade}}` β†’ `A`

{% hint style="info" %}
**Missing Variables**: If you don't provide a value for a variable, it's rendered as an empty string. This is useful for optional fields.
{% endhint %}

### Issuing the Same Template to Multiple Students

```typescript
const students = [
{ profileId: 'alice', name: 'Alice Smith', grade: 'A' },
{ profileId: 'bob', name: 'Bob Johnson', grade: 'B+' },
{ profileId: 'charlie', name: 'Charlie Brown', grade: 'A-' },
];

for (const student of students) {
await learnCard.invoke.send({
type: 'boost',
recipient: student.profileId,
templateUri: boostUri,
templateData: {
courseName: 'Web Development 101',
studentName: student.name,
grade: student.grade,
},
});
}
```

For more details on dynamic templates, see [Dynamic Templates with Mustache](../core-concepts/credentials-and-data/boost-credentials.md#dynamic-templates-with-mustache).

---

## Summary & What's Next

Fantastic! You've now learned how to: βœ… Understand the value of Boosts for reusable credential templates. βœ… Define the content for a Boost. βœ… Create a Boost using the LearnCard SDK. βœ… Send instances of that Boost to multiple recipients. βœ… Use the simplified `send` method for quick issuance.
Fantastic! You've now learned how to: βœ… Understand the value of Boosts for reusable credential templates. βœ… Define the content for a Boost. βœ… Create a Boost using the LearnCard SDK. βœ… Send instances of that Boost to multiple recipients. βœ… Use the simplified `send` method for quick issuance. βœ… Use dynamic templates with Mustache variables for personalized credentials.

Boosts are a powerful way to manage credentialing at scale. From here, you can explore:

* **Dynamic Templates:** Use Mustache variables for personalized credentials. (See [Dynamic Templates with Mustache](../core-concepts/credentials-and-data/boost-credentials.md#dynamic-templates-with-mustache)).
* **Retrieving Boost Recipients:** Use `learnCard.invoke.getPaginatedBoostRecipients(boostUri)` to see who has been issued a credential from this Boost.
* **Boost Permissions:** Control who can edit, issue, or manage your Boosts. (See Boost Permission Model).
* **Default Permissions:** Use `defaultPermissions` to create open Boosts that anyone can issue. (See [Default Permissions](../core-concepts/credentials-and-data/boost-credentials.md#default-permissions)).
Expand Down
1 change: 1 addition & 0 deletions packages/learn-card-types/src/lcn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ export const SendBoostInputValidator = z
templateUri: z.string().optional(),
template: SendBoostTemplateValidator.optional(),
signedCredential: VCValidator.optional(),
templateData: z.record(z.string(), z.unknown()).optional(),
})
.refine(data => data.templateUri || data.template, {
message: 'Either templateUri or template creation data must be provided.',
Expand Down
6 changes: 4 additions & 2 deletions packages/plugins/learn-card-network/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@
"esbuild-plugin-copy": "^1.3.0",
"jest": "^29.3.0",
"shx": "^0.3.4",
"ts-jest": "^29.0.3"
"ts-jest": "^29.0.3",
"@types/mustache": "^4.2.5"
},
"types": "./dist/index.d.ts",
"dependencies": {
"@learncard/core": "workspace:*",
"@learncard/helpers": "workspace:*",
"@learncard/network-brain-client": "workspace:*"
"@learncard/network-brain-client": "workspace:*",
"mustache": "^4.2.0"
}
}
Loading
Loading