A web-based tool for generating printable name badges and placards for Model United Nations (MUN) conferences. Upload tabular data (CSV, XLSX, or Google Sheets) and generate professional PDFs ready for printing.
- Three PDF templates
- Horizontal badge (85mm × 55mm) — for pinning
- Vertical badge (55mm × 85mm) — for lanyards
- Placard (A4 landscape) — table display with fold line
- Multiple input formats
- CSV files
- Excel spreadsheets (XLSX)
- Google Sheets (via public link)
- Built-in customization
- Country flags from ISO alpha-2 codes
- Custom images via image library
- Media consent indicators
- Optional barcode generation from ID field
- External integration via token-based session API
Your data source (CSV, XLSX, or Google Sheets) should contain these columns:
| Column | Required | Description |
|---|---|---|
name |
Yes | Person's full name |
countryName |
Yes | Display name (e.g., "Deutschland", "France") |
countryAlpha2Code |
No* | ISO 3166-1 alpha-2 code (e.g., "DE", "FR") or "UN" |
alternativeImage |
No* | Custom image name from the image library |
committee |
No | Committee name (shown in parentheses after name) |
pronouns |
No | Pronouns (e.g., "sie/ihr", "he/him") |
id |
No | ID number (generates barcode on vertical badges) |
mediaConsentStatus |
No | NOT_SET, NOT_ALLOWED, PARTIALLY_ALLOWED, ALLOWED_ALL |
*Either countryAlpha2Code or alternativeImage must be provided for each row.
To link users from your application to the badge generator with pre-filled data, use the token-based session API. This approach protects PII by using short-lived opaque tokens instead of exposing data in URL parameters.
The simplest integration method — no JavaScript required:
<form action="https://your-instance.example.com/api/session/create" method="POST">
<input type="hidden" name="name" value="Max Mustermann">
<input type="hidden" name="countryName" value="Deutschland">
<input type="hidden" name="countryAlpha2Code" value="DE">
<input type="hidden" name="committee" value="Generalversammlung">
<input type="hidden" name="pronouns" value="er/ihm">
<input type="hidden" name="id" value="12345">
<input type="hidden" name="mediaConsentStatus" value="ALLOWED_ALL">
<button type="submit">Badge erstellen</button>
</form>The form POST creates a session and redirects the user to /?t={token} with the form pre-filled.
For applications that need programmatic access:
async function openBadgeGenerator(userData) {
const response = await fetch('https://your-instance.example.com/api/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
const { token, url } = await response.json();
window.location.href = url;
}Session tokens expire after 15 minutes for privacy protection. Users will see a warning if they try to use an expired link.
- Bun runtime (v1.0+)
- Node.js 18+ (optional, for npm compatibility)
git clone <repository-url>
cd badge-generator
bun install
bun run devThe development server will start at http://localhost:5173.
| Command | Description |
|---|---|
bun run dev |
Start development server with hot reload |
bun run build |
Production build (runs prepareFlags.ts then vite build) |
bun run check |
Type checking with svelte-check |
bun run check:watch |
Type checking in watch mode |
bun run format |
Format code with Prettier |
bun run lint |
Check code formatting |
bun run preview |
Preview production build |
| Variable | Description | Default |
|---|---|---|
HOST |
Server host | 0.0.0.0 |
PORT |
Server port | 3000 |
ORIGIN |
Full origin URL for CORS | — |
DATA_DIR |
Directory for SQLite database | ./ |
For production deployments behind a reverse proxy, configure your adapter settings for proper header handling.
Build the image:
docker build -t badges:latest \
--build-arg VERSION=$(git describe --tags --always) \
--build-arg SHA=$(git rev-parse --short HEAD) \
.Run the container:
docker run -d \
-p 3000:3000 \
-v badges-data:/app/data \
-e ORIGIN=https://badges.yourdomain.com \
badges:latestThe /app/data volume persists the SQLite database for custom images.
This section explains how to fork and adapt the project for your own organization.
Follow these steps to add your organization's branding:
Place your logo files in the static/logo/ directory:
static/logo/
├── color/
│ └── your-brand.png # Main logo (square, ~200×200px recommended)
└── watermark/
└── your-brand.png # Watermark for placards (same dimensions)
Edit src/lib/types.ts and add your brand to the union type:
export type Brand = 'MUN-SH' | 'MUNBW' | 'DMUN' | 'UN' | 'YOUR-BRAND';Edit src/lib/pdfCommon.ts and add a case to the getBrandInfo function:
case 'YOUR-BRAND':
brandLogo = '/logo/color/your-brand.png';
primaryColor = '#FF5500'; // Your brand color (hex)
conferenceName = `Your Conference ${new Date().getFullYear()}`;
break;Edit src/lib/placardGeneration.ts and add a case to the watermark switch statement (around line 97):
case 'YOUR-BRAND':
brandLogo = '/logo/watermark/your-brand.png';
break;Edit src/routes/Preview.svelte and add an entry to the brandingTabs array:
const brandingTabs = [
{ title: 'MUN-SH', value: 'MUN-SH', icon: 'fa-solid fa-tag' },
{ title: 'MUNBW', value: 'MUNBW', icon: 'fa-solid fa-tag' },
{ title: 'DMUN', value: 'DMUN', icon: 'fa-solid fa-tag' },
{ title: 'United Nations', value: 'UN', icon: 'fa-solid fa-globe' },
{ title: 'Your Brand', value: 'YOUR-BRAND', icon: 'fa-solid fa-tag' } // Add this
] as const;If you want to remove Deutsche Model United Nations branding entirely:
- Remove unwanted brands from
src/lib/types.ts(Brand type) and related switch statements - Remove the hardcoded DMUN logo on vertical badges:
- In
src/lib/verticalBadgeGeneration.ts, remove lines 96 and 188-204 which fetch and drawsmall_dmun.pngat the bottom of badges
- In
- Update the default brand in
src/routes/Preview.svelte(line 24):const brandState = persistedState<Brand>('badge-generator-brand', 'YOUR-BRAND');
- Delete unused logo files from
static/logo/
The user interface is written in German. To translate:
- Search for German text strings in
src/routes/components - Replace with your preferred language
- Update validation messages in
src/lib/tableSchema.ts
Key files with translatable strings:
src/routes/Start.svelte— form labels and instructionssrc/routes/Preview.svelte— tab labelssrc/lib/tableSchema.ts— validation error messagessrc/lib/pdfCommon.ts— warning messages
Input (CSV/XLSX/Google Sheets)
→ Start.svelte (data upload + validation)
→ Zod schema validation (tableSchema.ts)
→ Preview.svelte (select PDF type + brand)
→ PDFPreviewer.svelte (orchestrates generation)
→ PDF Generator (placardGeneration.ts / verticalBadgeGeneration.ts / horizontalBadgeGeneration.ts)
→ Download
| Module | Purpose |
|---|---|
src/lib/tableSchema.ts |
Zod schema for input data validation |
src/lib/pdfCommon.ts |
Shared PDF utilities and brand configuration |
src/lib/placardGeneration.ts |
A4 placard generator (2-sided design) |
src/lib/verticalBadgeGeneration.ts |
55mm × 85mm lanyard badge |
src/lib/horizontalBadgeGeneration.ts |
85mm × 55mm pin badge |
src/lib/stores/ |
Svelte 5 stores for progress and warnings |
src/routes/api/img/ |
Custom image upload/retrieval API |
For detailed developer documentation, see CLAUDE.md.
SQLite needs to be compiled from source on arm64 systems. Since we don't deploy on arm64 systems yet, we took the easy way out and built the image for amd64 systems only. Please open an issue if you need an arm64 image.
See the Customization Guide above for step-by-step instructions on adding new brands, including logo files, type definitions, and UI elements.
Yes. Remove the brand from the Brand type in src/lib/types.ts, delete the corresponding cases from switch statements in pdfCommon.ts and placardGeneration.ts, remove the UI tab entry from Preview.svelte, and delete the unused logo files from static/logo/.
Configure your proxy to forward the appropriate headers (X-Forwarded-Host, X-Forwarded-Proto) and set the ORIGIN environment variable to your public URL.
Contributions are welcome. Please open an issue or submit a pull request with improvements. Feel free to reach out to the maintainers for any questions or concerns.
This project is licensed under the MIT License.