···11+MIT License
22+33+Copyright (c) 2025 Barry Prendergast
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+3-3
SETUP.md
···11-# Lanyard Setup Guide
11+# Lanyards Setup Guide
2233## Quick Start with App Password (Recommended for Development)
44···66771. Go to [https://bsky.app/settings/app-passwords](https://bsky.app/settings/app-passwords)
882. Click "Add App Password"
99-3. Give it a name (e.g., "Lanyard Development")
99+3. Give it a name (e.g., "Lanyards Development")
10104. Copy the generated password
11111212### 2. Configure Environment Variables
···46464747## Authentication Methods
48484949-Lanyard supports two authentication methods:
4949+Lanyards supports two authentication methods:
50505151### App Password (Development)
5252- **Pros**: Simple setup, no OAuth configuration needed
+98-81
lexicons/README.md
···11-# Lanyard Lexicons
11+# Lanyards Lexicons
2233This directory contains the AT Protocol lexicon definitions for Lanyard.
44+55+> [!NOTE]
66+> Lanyards uses the AT Protocol Lexicon CLI [@atproto/lex-cli](https://www.npmjs.com/package/@atproto/lex-cli) to automatically generate TypeScript types from the lexicon JSON definitions.
4758## Namespace Structure
69···811912```
1013at.lanyard/
1111-├── actor/ - Researcher identity and relationships
1212-│ ├── profile - Core researcher profile
1313-│ └── affiliation - Professional affiliations
1414-├── document/ - Research outputs
1515-│ └── work - Scholarly contributions (DOI-based)
1616-├── event/ - Academic activities
1717-│ └── academic - Conferences, symposiums, workshops
1818-├── link/ - External connections
1919-│ ├── social - Social network profiles
2020-│ └── web - Custom web links
2121-├── location/ - Geographic data (reusable)
2222-│ └── place - ISO country/region/city codes
2323-└── organization/ - Institutions (reusable)
2424- └── institution - Universities, companies (Ringgold/GRID)
1414+├── researcher - Core researcher profile (singleton record)
1515+├── work - Scholarly contributions (DOI-based, multiple records)
1616+├── event - Academic events (conferences, workshops, multiple records)
1717+├── link - External profiles and links (unified, multiple records)
1818+├── publication - Publication venues (journals, conferences, embedded object)
1919+├── organization - Institutions and entities (embedded object)
2020+└── location - Geographic locations (embedded object)
2521```
26222727-## Lexicon Hierarchy
2323+## Lexicon Details
28242929-### Core Objects (Independent)
2525+### Records (Top-level collections)
30263131-**`at.lanyard.location.place`**
3232-- Geographic location using ISO codes
3333-- Used by: profile, institution, event
3434-- Fields: country (ISO 3166-1), region (ISO 3166-2), city
2727+Records are stored as collections in the user's repository with `at-uri` identifiers.
35283636-**`at.lanyard.organization.institution`**
3737-- Academic/research institutions
3838-- Used by: affiliation, event (as organizer)
3939-- Identifiers: Ringgold ID, GRID ID
4040-- References: location.place
2929+> [!NOTE]
3030+> The following records are given as example. Referred to the lexicon themselves from more complete and up-to-date documentation.
41314242-### Actor (Researcher)
3232+**`at.lanyard.researcher`**
3333+- **Type**: Record (singleton, key: `literal:self`)
3434+- **Description**: The researcher's core profile and identity
3535+- **Required Fields**: `did`, `handle`, `createdAt`
3636+- **Optional Fields**: `displayName`, `avatar`, `description` (synced from Bluesky), `honorifics`, `location`, `affiliations`, `updatedAt`
3737+- **Embeds**: `location` (at.lanyard.location), `affiliations[]` (affiliation object)
3838+- **Subdefs**: `affiliation` - professional relationships with organizations
43394444-**`at.lanyard.actor.profile`**
4545-- Central researcher identity
4646-- Key: `self` (singleton record)
4747-- References: location.place
4848-- Synced from: Bluesky profile (avatar, displayName, description)
4949-- Additional: honorifics (Dr, Prof)
4040+**`at.lanyard.work`**
4141+- **Type**: Record (multiple, key: `tid`)
4242+- **Description**: Scholarly contributions identified by DOI
4343+- **Required Fields**: `doi`, `type`, `createdAt`
4444+- **Optional Fields**: `title`, `authors[]`, `publicationDate`, `venue`, `publication` (ref), `event` (at-uri ref)
4545+- **Work Types**: `abstract`, `poster`, `paper`, `conference-proceeding`, `journal-article`, `book-chapter`, `book`, `preprint`, `dataset`, `other`
4646+- **Note**: Metadata auto-fetched from CrossRef/DataCite via DOI
50475151-**`at.lanyard.actor.affiliation`**
5252-- Professional relationships
5353-- Key: `tid` (multiple records)
5454-- References: organization.institution
5555-- Fields: role, startDate, endDate, isPrimary
4848+**`at.lanyard.event`**
4949+- **Type**: Record (multiple, key: `tid`)
5050+- **Description**: Academic events where research is presented or discussed
5151+- **Required Fields**: `name`, `type`, `startDate`, `createdAt`
5252+- **Optional Fields**: `endDate`, `location` (ref), `organizer` (ref), `relatedWorks[]` (at-uri refs), `url`
5353+- **Event Types**: `conference`, `symposium`, `workshop`, `seminar`, `lecture`, `poster-session`, `webinar`, `other`
5454+- **Embeds**: `location` (at.lanyard.location), `organizer` (at.lanyard.organization)
56555757-### Links (External Connections)
5656+**`at.lanyard.link`**
5757+- **Type**: Record (multiple, key: `tid`)
5858+- **Description**: External profiles and custom web links (unified collection)
5959+- **Required Fields**: `url`, `type`, `createdAt`
6060+- **Optional Fields**: `platform`, `title`, `username`, `isLocked`
6161+- **Link Types**:
6262+ - `social` - Twitter, LinkedIn, Bluesky
6363+ - `academic` - ORCID, Google Scholar, ResearchGate, Semble
6464+ - `web` - Custom web links (max 3 per profile)
6565+- **Platforms**: `bluesky`, `twitter`, `linkedin`, `researchgate`, `googlescholar`, `orcid`, `semble`, `custom`
6666+- **Constraints**: 1 per social/academic platform, max 3 custom web links
58675959-**`at.lanyard.link.social`**
6060-- Social network profiles
6161-- Platforms: bluesky, twitter, linkedin, researchgate, googlescholar, semble
6262-- One per platform (Bluesky is locked/auto-synced)
6868+### Embedded Objects (Reusable components)
63696464-**`at.lanyard.link.web`**
6565-- Custom web links
6666-- Maximum 3 per profile
6767-- Fields: title, url
7070+Embedded objects are not stored as separate records but are embedded within other records.
68716969-### Research Output
7272+**`at.lanyard.location`**
7373+- **Type**: Object (embedded)
7474+- **Description**: Geographic location using ISO standard codes
7575+- **Fields**: `country` (ISO 3166-1 alpha-2), `region` (ISO 3166-2), `city`, `isVirtual`
7676+- **Used By**: researcher, organization, event
7777+- **Examples**: `{country: "US", region: "US-CA", city: "San Francisco"}`, `{isVirtual: true}`
70787171-**`at.lanyard.document.work`**
7272-- Scholarly contributions
7373-- Primary identifier: DOI
7474-- Metadata auto-fetched from CrossRef/DataCite
7575-- Types: paper, poster, abstract, dataset, etc.
7979+**`at.lanyard.organization`**
8080+- **Type**: Object (embedded)
8181+- **Description**: Institutions, publishers, societies, funders, companies
8282+- **Required Fields**: `name`
8383+- **Optional Fields**: `type`, `ringgoldId`, `gridId`, `rorId`, `location` (ref), `website`, `logo` (blob)
8484+- **Organization Types**: `institution`, `publisher`, `society`, `funder`, `company`, `government`, `other`
8585+- **Used By**: researcher.affiliation, event.organizer, publication.publisher
8686+- **Identifiers**: Ringgold ID (academic institutions), GRID ID, ROR ID
76877777-### Events
7878-7979-**`at.lanyard.event.academic`**
8080-- Conferences, symposiums, workshops
8181-- References: location.place, organization.institution (organizer)
8282-- Can link to: document.work[] (presented works)
8888+**`at.lanyard.publication`**
8989+- **Type**: Object (embedded)
9090+- **Description**: Publication venues (journals, conference proceedings, preprint servers)
9191+- **Required Fields**: `name`
9292+- **Optional Fields**: `type`, `issn`, `publisher` (ref), `website`, `subjects[]`
9393+- **Publication Types**: `journal`, `proceedings`, `preprint`, `repository`, `book-series`, `other`
9494+- **Used By**: work.publication
9595+- **Examples**: Nature, PLOS ONE, arXiv, NeurIPS Proceedings
83968497## Object Relationships
85988699```
8787-actor.profile
8888- ├─ refs → location.place (home location)
8989- ├─ has → actor.affiliation[] (multiple)
9090- └─ has → link.social[], link.web[]
100100+at.lanyard.researcher (record)
101101+ ├─ embeds → location (object)
102102+ └─ embeds → affiliations[] (objects)
103103+ └─ embed → organization (object)
104104+ └─ embed → location (object)
911059292-actor.affiliation
9393- └─ refs → organization.institution
106106+at.lanyard.work (record)
107107+ ├─ embeds → publication (object)
108108+ │ └─ embed → organization (object) [publisher]
109109+ └─ refs → event (at-uri) [optional]
941109595-organization.institution
9696- └─ refs → location.place
111111+at.lanyard.event (record)
112112+ ├─ embeds → location (object)
113113+ ├─ embeds → organizer (organization object)
114114+ └─ refs → relatedWorks[] (at-uri)
971159898-event.academic
9999- ├─ refs → organization.institution (organizer)
100100- ├─ refs → location.place (venue)
101101- └─ refs → document.work[] (presented)
116116+at.lanyard.link (record)
117117+ └─ (no references)
102118```
103119104120## Design Principles
105121106106-1. **Reusability** - location and organization are shared objects
107107-2. **Single Source of Truth** - No duplicated definitions
108108-3. **Clear Ownership** - actor.* owns links, affiliations
109109-4. **Modularity** - Each lexicon can evolve independently
110110-5. **AT Protocol Conventions** - Follows app.bsky.* patterns
122122+1. **Flat Namespace** - Simple top-level structure, no deep nesting
123123+2. **Embedded Objects** - Reusable components (location, organization, publication) embedded, not separate records
124124+3. **DOI-Centric** - Works primarily identified by DOI, metadata auto-fetched
125125+4. **Unified Links** - Single collection for social, academic, and custom links
126126+5. **AT Protocol Conventions** - Follows app.bsky.* patterns with records and embedded objects
127127+6. **Single Source of Truth** - Bluesky profile data is locked and synced
111128112129## Future Expansion
113130114131The structure allows for growth:
115115-- `at.lanyard.actor.education` - Academic degrees
116116-- `at.lanyard.document.grant` - Research funding
117117-- `at.lanyard.document.patent` - Patents
118118-- `at.lanyard.event.workshop` - Specific workshop type
119119-- `at.lanyard.organization.funder` - Funding bodies
132132+- `at.lanyard.education` - Academic degrees and credentials
133133+- `at.lanyard.grant` - Research funding records
134134+- `at.lanyard.patent` - Patent records
135135+- `at.lanyard.dataset` - Research datasets
136136+- `at.lanyard.teaching` - Teaching activities and courses
+10-17
src/app/[handle]/page.tsx
···2121 // Get Bluesky profile
2222 const bskyProfile = await agent.getProfile({ actor: did });
23232424- // Get Lanyard profile
2424+ // Get Lanyards profile
2525 const repo = new ResearcherRepository(agent);
2626 const lanyardProfile = await repo.getProfile(did);
2727···3131 <div className="text-center">
3232 <h1 className="text-2xl font-bold mb-4">Profile Not Found</h1>
3333 <p className="text-gray-600 mb-6">
3434- This user hasn't created a Lanyard profile yet.
3434+ This user hasn't created a Lanyards profile yet.
3535 </p>
3636- <a
3737- href="/"
3838- className="text-blue-600 hover:underline"
3939- >
3636+ <a href="/" className="text-blue-600 hover:underline">
4037 Go to homepage
4138 </a>
4239 </div>
···4542 }
46434744 // Get all profile data
4848- const [affiliations, webLinks, works, events] =
4949- await Promise.all([
5050- repo.listAffiliations(did),
5151- repo.listWebLinks(did),
5252- repo.listWorks(did),
5353- repo.listEvents(did),
5454- ]);
4545+ const [affiliations, webLinks, works, events] = await Promise.all([
4646+ repo.listAffiliations(did),
4747+ repo.listWebLinks(did),
4848+ repo.listWorks(did),
4949+ repo.listEvents(did),
5050+ ]);
55515652 return (
5753 <ProfileView
···7672 <p className="text-gray-600 mb-6">
7773 Unable to load this profile. Please try again later.
7874 </p>
7979- <a
8080- href="/"
8181- className="text-blue-600 hover:underline"
8282- >
7575+ <a href="/" className="text-blue-600 hover:underline">
8376 Go to homepage
8477 </a>
8578 </div>
+1-1
src/app/layout.tsx
···22import './globals.css';
3344export const metadata: Metadata = {
55- title: 'Lanyard - Researcher Profiles on AT Protocol',
55+ title: 'Lanyards - Researcher Profiles on AT Protocol',
66 description:
77 'A dedicated profile for researchers, built on the AT Protocol. An alternative to ORCID.',
88};
···107107 </button>
108108109109 <p className="text-xs text-gray-500 text-center">
110110- Sign in to access your Lanyard profile
110110+ Sign in to access your Lanyards profile
111111 </p>
112112 </form>
113113 );
+97
src/types/README.md
···11+## Lanyards Types Generation
22+33+Lanyards uses the AT Protocol Lexicon CLI [@atproto/lex-cli](https://www.npmjs.com/package/@atproto/lex-cli) to automatically generate TypeScript types from the lexicon JSON definitions.
44+55+### Generated Files
66+77+When you run the code generation, the following files are created in `src/types/generated/`:
88+99+```
1010+src/types/generated/
1111+├── index.ts - Main export file with all types
1212+├── lexicons.ts - Lexicon definitions for runtime validation
1313+├── util.ts - Utility types and helpers
1414+└── types/at/lanyard/ - Generated type definitions
1515+ ├── researcher.ts - Researcher record types
1616+ ├── work.ts - Work record types
1717+ ├── event.ts - Event record types
1818+ ├── link.ts - Link record types
1919+ ├── publication.ts - Publication object types
2020+ ├── organization.ts - Organization object types
2121+ └── location.ts - Location object types
2222+```
2323+2424+> [!NOTE]
2525+> The types are **NOT** committed to git in most workflows, but are generated as part of the build process.
2626+2727+### Commands
2828+2929+**Generate types once:**
3030+```bash
3131+npm run lex:gen
3232+```
3333+This command reads all `.json` files in the `lexicons/` directory and generates TypeScript types in `src/types/generated/`.
3434+3535+**Watch mode (development):**
3636+```bash
3737+npm run lex:watch
3838+```
3939+Watches for changes to lexicon files and automatically regenerates types.
4040+4141+**Build process:**
4242+```bash
4343+npm run build
4444+```
4545+The build command automatically runs `lex:gen` before building the Next.js app to ensure types are up-to-date.
4646+4747+### How It Works
4848+4949+1. **Lexicon Definitions**: JSON files in `lexicons/` define the schema using AT Protocol lexicon syntax
5050+2. **CLI Tool**: `@atproto/lex-cli` parses the JSON definitions
5151+3. **Type Generation**: Creates TypeScript interfaces, types, and validation schemas
5252+4. **Import & Use**: Generated types are imported throughout the codebase via `@/types/generated`
5353+5454+### Generated Type Structure
5555+5656+Each generated file includes:
5757+- **Record types**: For top-level collection records (researcher, work, event, link)
5858+- **Object types**: For embedded objects (location, organization, publication)
5959+- **Input/Output schemas**: For API operations (create, update, delete)
6060+- **Validation functions**: Runtime validation using the lexicon definitions
6161+6262+### Example Usage
6363+6464+```typescript
6565+import {
6666+ Researcher,
6767+ Work,
6868+ Event,
6969+ Link
7070+} from '@/types/generated';
7171+7272+// Use generated types in your code
7373+const researcher: Researcher.Record = {
7474+ did: 'did:plc:...',
7575+ handle: 'researcher.bsky.social',
7676+ createdAt: new Date().toISOString(),
7777+ // ... other fields
7878+};
7979+```
8080+8181+### Type Declaration Fixes
8282+8383+Due to import path resolution in generated types, we maintain custom type declarations in `src/types/`:
8484+- **`atproto.d.ts`**: Namespace declarations for AT Protocol repo operations
8585+- **`multiformats.d.ts`**: Module declaration for CID imports
8686+8787+These ensure the generated types work correctly with the AT Protocol SDK.
8888+8989+### Regenerating Types
9090+9191+You should regenerate types whenever you:
9292+1. **Modify lexicon files**: Add/remove fields, change types, update descriptions
9393+2. **Add new lexicons**: Create new `.json` files in `lexicons/`
9494+3. **Change constraints**: Update validation rules (maxLength, enum values, etc.)
9595+4. **Pull changes**: After pulling changes that include lexicon updates
9696+9797+
+1-1
src/types/index.ts
···11/**
22- * Type exports for Lanyard application
22+ * Type exports for Lanyards application
33 * Re-exports generated AT Protocol lexicon types with convenient aliases
44 */
55