Personal Knowledge Management System
Linky is a modern personal knowledge management system featuring efficient bookmark management, memos, an elegant user interface, and secure authentication.
简体中文 | English
- 📚 Multi-dimensional organization with categories and tags
- 🔍 Powerful search and filtering capabilities
- 👁️ Visit count tracking
- 📊 Three view modes: masonry, grid, list
- 🏷️ Custom tag system
- 📝 Rich text editing (Markdown support)
- ⭐ Pin important memos
- 🏷️ Tag-based categorization
- 📅 Quick date display (today/yesterday)
- 🔍 Search and sorting functionality
- ✉️ Email registration and login
- 🔒 Access Token authentication (stateless)
- 📧 Email verification
- 🔑 Forgot password / reset password
- 🔄 Resend verification email
- Framework: AdonisJS 6.x
- Language: TypeScript
- Database: PostgreSQL
- ORM: Lucid ORM
- Authentication: Access Token (Bearer Token)
- Validation: VineJS
- Testing: Japa
- Mail: AdonisJS Mail
- Framework: Nuxt 4.x
- Language: TypeScript
- UI Library: Nuxt UI 4.x (built on Tailwind CSS)
- Styling: Tailwind CSS 4.x
- HTTP Client: Nitro ($fetch)
- Icons: Heroicons, Lucide
- Package Manager: pnpm workspace
- Code Quality: ESLint + Prettier
- Type Checking: TypeScript compiler
- Version Control: Git
Linky/
├── backend/ # AdonisJS backend application
│ ├── app/
│ │ ├── controllers/ # HTTP route handlers
│ │ │ └── auth_controller.ts
│ │ ├── models/ # Database models (Lucid ORM)
│ │ │ └── user.ts
│ │ ├── middleware/ # HTTP middleware
│ │ │ ├── auth_middleware.ts
│ │ │ └── force_json_response_middleware.ts
│ │ ├── validators/ # Request validators (VineJS)
│ │ │ ├── login.ts
│ │ │ ├── register.ts
│ │ │ └── reset_password.ts
│ │ ├── services/ # Business logic services
│ │ │ └── auth_service.ts
│ │ ├── mails/ # Email templates
│ │ │ ├── verify_email_notification.ts
│ │ │ └── reset_password_notification.ts
│ │ └── exceptions/ # Custom exceptions
│ ├── config/ # Configuration files
│ │ ├── auth.ts # Authentication configuration
│ │ ├── database.ts # Database connection
│ │ ├── mail.ts # Email service configuration
│ │ └── cors.ts # CORS configuration
│ ├── database/
│ │ └── migrations/ # Database migration files
│ ├── start/ # Startup files
│ │ ├── routes.ts # API route definitions
│ │ ├── kernel.ts # Middleware registration
│ │ └── env.ts # Environment variable validation
│ ├── tests/ # Test files (Japa)
│ └── package.json
│
├── web/ # Nuxt 4.x frontend application
│ ├── app/
│ │ ├── pages/ # File-based routing
│ │ │ ├── index.vue
│ │ │ ├── auth/
│ │ │ │ ├── sign-in.vue
│ │ │ │ ├── sign-up.vue
│ │ │ │ ├── forgot-password.vue
│ │ │ │ ├── reset-password.vue
│ │ │ │ └── verify-email.vue
│ │ │ └── workspace/
│ │ │ ├── bookmarks.vue
│ │ │ └── memos.vue
│ │ ├── layouts/ # Page layouts
│ │ │ ├── default.vue
│ │ │ ├── auth.vue
│ │ │ ├── marketing.vue
│ │ │ └── workspace.vue
│ │ ├── components/ # Vue components
│ │ │ ├── BookmarkCard.vue
│ │ │ ├── MemoCard.vue
│ │ │ ├── TagsInput.vue
│ │ │ └── shared/
│ │ ├── middleware/ # Route middleware
│ │ │ └── auth.global.ts
│ │ ├── composables/ # Vue composables
│ │ │ ├── useAuth.ts
│ │ │ └── useHttpError.ts
│ │ ├── api/ # API client modules
│ │ │ ├── auth.ts
│ │ │ └── types.ts
│ │ ├── lib/ # Utilities
│ │ │ └── request.ts # HTTP request wrapper
│ │ └── assets/ # Static assets
│ │ └── css/
│ ├── public/ # Static files
│ ├── nuxt.config.ts # Nuxt configuration
│ └── package.json
│
├── package.json # Root package (workspace manager)
├── pnpm-workspace.yaml # Workspace configuration
├── AGENTS.md # AI assistant guidelines
├── openspec/ # OpenSpec documentation
└── .opencode/ # AI coding rules
├── rules/ # Development rules
│ ├── 00-overview.md
│ ├── 01-common.md
│ ├── 02-backend.md
│ └── 03-frontend.md
└── skills/ # Framework-specific skills
├── adonisjs/
├── nuxt/
├── nuxt-ui/
└── tailwindcss/
- Node.js >= 20.0.0
- pnpm >= 10.26.1
- PostgreSQL
pnpm installBackend environment variables (backend/.env):
PORT=3333
APP_KEY=your-app-key-here
NODE_ENV=development
# Database
PG_HOST=127.0.0.1
PG_PORT=5432
PG_USER=postgres
PG_PASSWORD=your_password
PG_DATABASE=linky
# Mail (optional)
MAIL_SMTP_HOST=smtp.gmail.com
MAIL_SMTP_PORT=587
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-passwordFrontend environment variables (web/.env):
NUXT_PUBLIC_API_BASE_URL=http://localhost:3333
NUXT_PUBLIC_APP_NAME=Linkycd backend
node ace migration:runStart backend server (port 3333):
pnpm run dev:backendStart frontend server (port 3000):
pnpm run dev:webVisit the app: http://localhost:3000
pnpm run buildStart production backend:
pnpm run startPreview production frontend:
pnpm run previewPOST /api/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"name": "John Doe",
"password": "password123"
}
Response 200:
{
"user": { ... },
"token": "access_token_here"
}POST /api/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}
Response 200:
{
"user": { ... },
"token": "access_token_here"
}POST /api/auth/logout
Authorization: Bearer {token}
Response 200:
{
"success": true
}GET /api/auth/me
Authorization: Bearer {token}
Response 200:
{
"id": 1,
"email": "user@example.com",
"fullName": "John Doe",
...
}POST /api/auth/forgot-password
Content-Type: application/json
{
"email": "user@example.com"
}
Response 200:
{
"success": true,
"message": "Reset email has been sent"
}POST /api/auth/reset-password
Content-Type: application/json
{
"token": "reset_token_here",
"password": "new_password"
}
Response 200:
{
"success": true
}GET /api/auth/verify-email?token=verification_token_here
Response 200:
{
"success": true,
"message": "Email verified successfully"
}POST /api/auth/resend-verification
Authorization: Bearer {token}
Response 200:
{
"success": true,
"message": "Verification email has been sent"
}# Lint all code
pnpm run lint
# Backend only
pnpm --filter backend lint
# Frontend only
pnpm --filter web lint
# Auto-fix
pnpm --filter backend lint --fix
pnpm --filter web lint --fix# Type check all code
pnpm run typecheck
# Backend only
pnpm --filter backend typecheck
# Frontend only
pnpm --filter web typecheck# Run backend tests
pnpm --filter backend testLinky uses an Access Token authentication system (stateless):
-
Login Flow:
- User submits credentials to
/api/auth/login - Backend validates and returns JWT access token
- Frontend stores token in
auth_tokencookie
- User submits credentials to
-
Request Authentication:
- Each request automatically carries
Authorization: Bearer {token}header - Backend validates token and retrieves user information
- Protected routes use
middleware.auth()middleware
- Each request automatically carries
-
Token Storage:
- Frontend:
auth_tokencookie (30-day expiration) - Backend:
access_tokensdatabase table
- Frontend:
- File names:
kebab-case(e.g.,auth_middleware.ts,user-profile.vue) - Component names:
PascalCase(e.g.,UserProfile,TagsInput) - Class/Interface names:
PascalCase(e.g.,UserController,AuthService) - Function/Variable names:
camelCase(e.g.,getUserData,isAuthenticated) - Constants:
SCREAMING_SNAKE_CASE(e.g.,MAX_RETRY_COUNT)
- Indentation: 2 spaces
- Quotes: Single quotes
- Line width: 100 characters
- Semicolons: No semicolons
- Trailing commas: No trailing commas
- Nuxt UI components: Use kebab-case (e.g.,
<u-button>,<u-input>) - Custom components: Use kebab-case (e.g.,
<bookmark-card>,<memo-card>) - Layout components: Use kebab-case (e.g.,
<workspace-layout>,<auth-layout>)
cd backend
# Create controller
node ace make:controller UserController
# Create model
node ace make:model Bookmark
# Create migration
node ace make:migration create_bookmarks_table
# Create validator
node ace make:validator CreateBookmark
# Create service
node ace make:service BookmarkService
# Create middleware
node ace make:middleware AuthMiddleware# Run pending migrations
node ace migration:run
# Rollback last migration
node ace migration:rollback# Run all tests
pnpm --filter backend test
# Run specific test file
node ace test --files="**/auth.spec.ts"Create .vue files in app/pages/, routes are auto-generated:
pages/
├── index.vue # /
├── auth/
│ ├── sign-in.vue # /auth/sign-in
│ └── sign-up.vue # /auth/sign-up
└── workspace/
├── bookmarks.vue # /workspace/bookmarks
└── memos.vue # /workspace/memos
Create Vue components in app/components/:
<script setup lang="ts">
defineProps<{
title: string
}>()
</script>
<template>
<div>{{ title }}</div>
</template>Use the wrapped request utility:
import { request } from '~/lib/request'
// GET
const user = await request.get<User>('/auth/me')
// POST
const result = await request.post<{ message: string }>('/auth/login', {
email,
password
})Create reusable logic in app/composables/:
export const useAuth = () => {
const user = ref<User | null>(null)
const login = async (credentials: LoginRequest) => {
// ...
}
return { user, login }
}cd backend
node ace migration:rollback --batch=0
node ace migration:runcd backend
node ace generate:keyRefer to app/composables/useHttpError.ts, it automatically redirects to the login page.
- Create using
node ace make:middleware - Register in
start/kernel.ts - Use in routes:
.middleware(middleware.auth())
- Fork this repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Linky is dual-licensed under the following terms:
- Free for personal, educational, and non-commercial use
- Modifications must be shared under the same license
- Suitable for open-source projects and internal tools
- Use in commercial products without making your code public
- Priority support and custom features
- Remove attribution requirements
- Contact us for pricing and terms
For commercial licensing inquiries, please contact us at:
- Email: zhaoguiyang18@outlook.com
- Website: https://linky.zhaoguiyang.com
For full license details, see the LICENSE file.
- AdonisJS Official Docs: https://docs.adonisjs.com
- Nuxt Official Docs: https://nuxt.com/docs
- Nuxt UI Docs: https://ui.nuxt.com
- Tailwind CSS Docs: https://tailwindcss.com/docs
For questions or suggestions, please open an Issue.