···11+# Lanyard
22+33+A dedicated profile for researchers, built on the AT Protocol.
44+55+Researchers will use this as an alternative to the ORCID ID.
66+77+## Features
88+99+- **Account Creation**: Sign in with your Bluesky account using OAuth
1010+- **Researcher Profile**: Mobile-first profile display with QR code sharing
1111+- **Profile Management**: Manage honorifics, location, affiliations, and more
1212+- **Social Networks**: Link to Twitter, LinkedIn, ResearchGate, Google Scholar, and Semble
1313+- **Scholarly Contributions**: Add research using DOIs
1414+- **Academic Events**: Track conference presentations and symposiums
1515+1616+## Technology Stack
1717+1818+- Next.js (latest)
1919+- TypeScript
2020+- Tailwind CSS v4
2121+- AT Protocol (@atproto/*)
2222+- Zod for validation
2323+2424+## Getting Started
2525+2626+### Quick Start (App Password)
2727+2828+1. Install dependencies:
2929+ ```bash
3030+ npm install
3131+ ```
3232+3333+2. Set up your Bluesky app password:
3434+ ```bash
3535+ cp .env.example .env
3636+ # Edit .env and add your BLUESKY_HANDLE and BLUESKY_APP_PASSWORD
3737+ ```
3838+3939+3. Run the development server:
4040+ ```bash
4141+ npm run dev
4242+ ```
4343+4444+4. Open [http://localhost:3000](http://localhost:3000)
4545+4646+See [SETUP.md](SETUP.md) for detailed setup instructions and OAuth configuration.
4747+4848+## Development
4949+5050+- `npm run dev` - Start development server
5151+- `npm run build` - Build for production
5252+- `npm run start` - Start production server
5353+- `npm run lint` - Run ESLint
5454+- `npm run format` - Format code with Prettier
+104
SETUP.md
···11+# Lanyard Setup Guide
22+33+## Quick Start with App Password (Recommended for Development)
44+55+### 1. Create a Bluesky App Password
66+77+1. Go to [https://bsky.app/settings/app-passwords](https://bsky.app/settings/app-passwords)
88+2. Click "Add App Password"
99+3. Give it a name (e.g., "Lanyard Development")
1010+4. Copy the generated password
1111+1212+### 2. Configure Environment Variables
1313+1414+1. Copy the example environment file:
1515+ ```bash
1616+ cp .env.example .env
1717+ ```
1818+1919+2. Edit `.env` and add your credentials:
2020+ ```env
2121+ # Set authentication method to app_password
2222+ AUTH_METHOD=app_password
2323+2424+ # Add your Bluesky credentials
2525+ BLUESKY_HANDLE=your-handle.bsky.social
2626+ BLUESKY_APP_PASSWORD=your-app-password-here
2727+2828+ # Leave these as-is for now
2929+ NEXT_PUBLIC_APP_URL=http://localhost:3000
3030+ PDS_URL=https://bsky.social
3131+ ```
3232+3333+### 3. Install Dependencies
3434+3535+```bash
3636+npm install
3737+```
3838+3939+### 4. Run the Development Server
4040+4141+```bash
4242+npm run dev
4343+```
4444+4545+Visit [http://localhost:3000](http://localhost:3000) and click "Sign In". You'll be automatically authenticated with your configured account.
4646+4747+## Authentication Methods
4848+4949+Lanyard supports two authentication methods:
5050+5151+### App Password (Development)
5252+- **Pros**: Simple setup, no OAuth configuration needed
5353+- **Cons**: Single account only, credentials in .env file
5454+- **Use for**: Local development and testing
5555+- **Configuration**: Set `AUTH_METHOD=app_password` in `.env`
5656+5757+### OAuth (Production)
5858+- **Pros**: Multi-user support, secure authorization flow
5959+- **Cons**: Requires OAuth client setup and configuration
6060+- **Use for**: Production deployment
6161+- **Configuration**: Set `AUTH_METHOD=oauth` in `.env`
6262+6363+## Switching Between Authentication Methods
6464+6565+Simply change the `AUTH_METHOD` value in your `.env` file:
6666+6767+```env
6868+# For App Password
6969+AUTH_METHOD=app_password
7070+7171+# For OAuth
7272+AUTH_METHOD=oauth
7373+```
7474+7575+Restart the development server after changing the method.
7676+7777+## Setting Up OAuth (Production)
7878+7979+Coming soon - OAuth setup requires AT Protocol OAuth client configuration.
8080+8181+## Troubleshooting
8282+8383+### "App password not configured" error
8484+- Check that `BLUESKY_HANDLE` and `BLUESKY_APP_PASSWORD` are set in `.env`
8585+- Ensure there are no extra spaces or quotes around the values
8686+- Restart the development server after changing `.env`
8787+8888+### Authentication fails
8989+- Verify your app password is correct
9090+- Make sure your Bluesky account is active
9191+- Check that `PDS_URL` is set to `https://bsky.social`
9292+9393+### Can't access dashboard
9494+- Make sure you're signed in (check for session cookie)
9595+- Try clearing cookies and signing in again
9696+- Check the browser console for errors
9797+9898+## Next Steps
9999+100100+Once authenticated, you can:
101101+1. View your dashboard at `/dashboard`
102102+2. Add affiliations, publications, and events
103103+3. Configure your profile settings
104104+4. View your public profile at `/{your-handle}`
+119
lexicons/README.md
···11+# Lanyard Lexicons
22+33+This directory contains the AT Protocol lexicon definitions for Lanyard.
44+55+## Namespace Structure
66+77+All lexicons use the `at.lanyard.*` namespace in anticipation of the `lanyard.at` domain.
88+99+```
1010+at.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)
2525+```
2626+2727+## Lexicon Hierarchy
2828+2929+### Core Objects (Independent)
3030+3131+**`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
3535+3636+**`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
4141+4242+### Actor (Researcher)
4343+4444+**`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)
5050+5151+**`at.lanyard.actor.affiliation`**
5252+- Professional relationships
5353+- Key: `tid` (multiple records)
5454+- References: organization.institution
5555+- Fields: role, startDate, endDate, isPrimary
5656+5757+### Links (External Connections)
5858+5959+**`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)
6363+6464+**`at.lanyard.link.web`**
6565+- Custom web links
6666+- Maximum 3 per profile
6767+- Fields: title, url
6868+6969+### Research Output
7070+7171+**`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.
7676+7777+### 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)
8383+8484+## Object Relationships
8585+8686+```
8787+actor.profile
8888+ ├─ refs → location.place (home location)
8989+ ├─ has → actor.affiliation[] (multiple)
9090+ └─ has → link.social[], link.web[]
9191+9292+actor.affiliation
9393+ └─ refs → organization.institution
9494+9595+organization.institution
9696+ └─ refs → location.place
9797+9898+event.academic
9999+ ├─ refs → organization.institution (organizer)
100100+ ├─ refs → location.place (venue)
101101+ └─ refs → document.work[] (presented)
102102+```
103103+104104+## Design Principles
105105+106106+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
111111+112112+## Future Expansion
113113+114114+The 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
+74
lexicons/event/event.json
···11+{
22+ "lexicon": 1,
33+ "id": "at.lanyard.event",
44+ "defs": {
55+ "main": {
66+ "type": "record",
77+ "description": "An academic event such as a conference, symposium, workshop, or seminar where research is presented or discussed",
88+ "key": "tid",
99+ "record": {
1010+ "type": "object",
1111+ "required": ["name", "type", "startDate", "createdAt"],
1212+ "properties": {
1313+ "name": {
1414+ "type": "string",
1515+ "maxLength": 300,
1616+ "description": "Event name (e.g., 'NeurIPS 2024', 'Annual Biology Symposium')"
1717+ },
1818+ "type": {
1919+ "type": "string",
2020+ "enum": [
2121+ "conference",
2222+ "symposium",
2323+ "workshop",
2424+ "seminar",
2525+ "lecture",
2626+ "poster-session",
2727+ "webinar",
2828+ "other"
2929+ ],
3030+ "description": "Type of academic event"
3131+ },
3232+ "startDate": {
3333+ "type": "string",
3434+ "format": "datetime",
3535+ "description": "Event start date"
3636+ },
3737+ "endDate": {
3838+ "type": "string",
3939+ "format": "datetime",
4040+ "description": "Event end date (optional, for multi-day events)"
4141+ },
4242+ "location": {
4343+ "type": "ref",
4444+ "ref": "at.lanyard.location#main",
4545+ "description": "Event location (can be physical or virtual)"
4646+ },
4747+ "organizer": {
4848+ "type": "ref",
4949+ "ref": "at.lanyard.organization#main",
5050+ "description": "Organization that organized/hosted the event"
5151+ },
5252+ "relatedWorks": {
5353+ "type": "array",
5454+ "items": {
5555+ "type": "string",
5656+ "format": "at-uri"
5757+ },
5858+ "description": "AT-URI references to related work records presented at this event"
5959+ },
6060+ "url": {
6161+ "type": "string",
6262+ "format": "uri",
6363+ "description": "Event website URL"
6464+ },
6565+ "createdAt": {
6666+ "type": "string",
6767+ "format": "datetime",
6868+ "description": "Timestamp when this event record was created"
6969+ }
7070+ }
7171+ }
7272+ }
7373+ }
7474+}
+64
lexicons/link/link.json
···11+{
22+ "lexicon": 1,
33+ "id": "at.lanyard.link",
44+ "defs": {
55+ "main": {
66+ "type": "record",
77+ "description": "A link to an external profile or website - can be a social network profile, academic profile, or custom web link. Limits per profile: 1 per social platform, max 3 custom web links",
88+ "key": "tid",
99+ "record": {
1010+ "type": "object",
1111+ "required": ["url", "type", "createdAt"],
1212+ "properties": {
1313+ "type": {
1414+ "type": "string",
1515+ "enum": [
1616+ "social",
1717+ "academic",
1818+ "web"
1919+ ],
2020+ "description": "Type of link - social (Twitter, LinkedIn), academic (ORCID, Scholar), or web (custom links)"
2121+ },
2222+ "platform": {
2323+ "type": "string",
2424+ "enum": [
2525+ "bluesky",
2626+ "twitter",
2727+ "linkedin",
2828+ "researchgate",
2929+ "googlescholar",
3030+ "orcid",
3131+ "semble",
3232+ "custom"
3333+ ],
3434+ "description": "Platform identifier for social/academic links (use 'custom' for web type)"
3535+ },
3636+ "url": {
3737+ "type": "string",
3838+ "format": "uri",
3939+ "description": "Full URL to the profile or website"
4040+ },
4141+ "title": {
4242+ "type": "string",
4343+ "maxLength": 100,
4444+ "description": "Display title for the link (primarily for custom web links)"
4545+ },
4646+ "username": {
4747+ "type": "string",
4848+ "maxLength": 100,
4949+ "description": "Username or identifier on the platform (for social/academic links)"
5050+ },
5151+ "isLocked": {
5252+ "type": "boolean",
5353+ "description": "Whether this link can be edited/deleted (Bluesky profile is locked, derived from auth)"
5454+ },
5555+ "createdAt": {
5656+ "type": "string",
5757+ "format": "datetime",
5858+ "description": "Timestamp when this link was created"
5959+ }
6060+ }
6161+ }
6262+ }
6363+ }
6464+}
+29
lexicons/location/location.json
···11+{
22+ "lexicon": 1,
33+ "id": "at.lanyard.location",
44+ "defs": {
55+ "main": {
66+ "type": "object",
77+ "description": "A geographic location using ISO standard codes - can represent a researcher's home country, organization location, or event venue",
88+ "properties": {
99+ "country": {
1010+ "type": "string",
1111+ "description": "ISO 3166-1 alpha-2 country code (e.g., US, GB, AT, DE, JP)"
1212+ },
1313+ "region": {
1414+ "type": "string",
1515+ "description": "ISO 3166-2 region/state/province code (e.g., US-CA for California, GB-ENG for England) - optional"
1616+ },
1717+ "city": {
1818+ "type": "string",
1919+ "maxLength": 100,
2020+ "description": "City name (optional)"
2121+ },
2222+ "isVirtual": {
2323+ "type": "boolean",
2424+ "description": "Whether this is a virtual/online location (for virtual events)"
2525+ }
2626+ }
2727+ }
2828+ }
2929+}
···11+/**
22+ * OAuth Client Placeholder
33+ *
44+ * The @atproto/oauth-client-node package has a complex setup that requires:
55+ * - Proper client metadata configuration
66+ * - State and session stores
77+ * - DPoP (Demonstrated Proof of Possession) setup
88+ *
99+ * For now, this is a placeholder. To implement properly:
1010+ * 1. Follow the AT Protocol OAuth documentation
1111+ * 2. Set up proper session and state storage
1212+ * 3. Configure client metadata correctly
1313+ *
1414+ * Alternative: Use direct API authentication instead of OAuth for simpler setup
1515+ */
1616+1717+export async function getOAuthClient() {
1818+ throw new Error(
1919+ 'OAuth client not yet fully implemented. Please configure OAuth according to AT Protocol documentation.'
2020+ );
2121+}
2222+2323+export async function createAuthUrl(_handle: string): Promise<string> {
2424+ throw new Error(
2525+ 'OAuth client not yet fully implemented. Please configure OAuth according to AT Protocol documentation.'
2626+ );
2727+}
+46
src/lib/auth/oauth-client.ts
···11+/**
22+ * OAuth Client Configuration
33+ *
44+ * Note: The @atproto/oauth-client-node package requires complex setup.
55+ * This is a temporary type-safe placeholder implementation.
66+ *
77+ * To properly implement OAuth:
88+ * 1. Review AT Protocol OAuth documentation
99+ * 2. Set up NodeSavedStateStore and NodeSavedSessionStore
1010+ * 3. Configure client metadata properly
1111+ * 4. Handle DPoP tokens
1212+ *
1313+ * For initial development, consider using app passwords or direct API auth
1414+ */
1515+1616+// Placeholder type
1717+export type OAuthClient = {
1818+ authorize: (handle: string) => Promise<string>;
1919+ callback: (params: URLSearchParams) => Promise<{
2020+ session: {
2121+ sub: string;
2222+ scope: string;
2323+ };
2424+ state: string | null;
2525+ }>;
2626+};
2727+2828+let oauthClient: OAuthClient | null = null;
2929+3030+export async function getOAuthClient(): Promise<OAuthClient> {
3131+ if (oauthClient) {
3232+ return oauthClient;
3333+ }
3434+3535+ // TODO: Implement proper OAuth client initialization
3636+ // See @atproto/oauth-client-node documentation
3737+3838+ throw new Error(
3939+ 'OAuth client not yet fully configured. Please set up OAuth according to AT Protocol documentation.'
4040+ );
4141+}
4242+4343+export async function createAuthUrl(handle: string): Promise<string> {
4444+ const client = await getOAuthClient();
4545+ return await client.authorize(handle);
4646+}
···11+/**
22+ * Type exports for Lanyard application
33+ * Re-exports generated AT Protocol lexicon types with convenient aliases
44+ */
55+66+import type {
77+ AtLanyardResearcher,
88+ AtLanyardWork,
99+ AtLanyardEvent,
1010+ AtLanyardLink,
1111+ AtLanyardOrganization,
1212+ AtLanyardPublication,
1313+ AtLanyardLocation,
1414+} from './generated';
1515+1616+// Main record types
1717+export type Researcher = AtLanyardResearcher.Record;
1818+export type Work = AtLanyardWork.Record;
1919+export type Event = AtLanyardEvent.Record;
2020+export type Link = AtLanyardLink.Record;
2121+export type Organization = AtLanyardOrganization.Main;
2222+export type Publication = AtLanyardPublication.Main;
2323+export type Location = AtLanyardLocation.Main;
2424+2525+// Nested types
2626+export type Affiliation = AtLanyardResearcher.Affiliation;
2727+2828+// Convenience type aliases for enums and unions
2929+export type Honorific = 'Dr' | 'Prof';
3030+export type WorkType = Work['type'];
3131+export type EventType = Event['type'];
3232+export type LinkType = Link['type'];
3333+export type LinkPlatform = NonNullable<Link['platform']>;
3434+export type OrganizationType = NonNullable<Organization['type']>;
3535+export type PublicationType = NonNullable<Publication['type']>;
+5
src/types/multiformats.d.ts
···11+// Type declarations for multiformats package
22+// This resolves import issues with the generated AT Protocol types
33+declare module 'multiformats/cid' {
44+ export { CID } from 'multiformats';
55+}