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
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,13 @@ For comprehensive guides, API reference, and examples, visit our documentation:

**📖 [https://sequelize-guard.js.org](https://sequelize-guard.js.org)**

**📖 (Fallback: [https://sequelize-guard.vercel.app](https://sequelize-guard.vercel.app))**

### Key Topics

- **[Getting Started](https://sequelize-guard.js.org/getting-started)** - Installation and basic setup
- **[API Reference](https://sequelize-guard.js.org/api)** - Complete API documentation
- **[TypeScript Support](https://sequelize-guard.js.org/typescript)** - Using with TypeScript
- **[Examples](https://sequelize-guard.js.org/examples)** - Real-world usage examples
- **[Blog](https://sequelize-guard.js.org/blogs)** - Articles and insights about the project

### Migration

Expand Down Expand Up @@ -223,7 +222,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file

Need help? Here's how to get support:

- 📖 **Documentation:** [https://sequelize-guard.js.org](https://sequelize-guard.js.org) (or [https://sequelize-guard.vercel.app](https://sequelize-guard.vercel.app))
- 📖 **Documentation:** [https://sequelize-guard.js.org](https://sequelize-guard.js.org) (fallback: [https://sequelize-guard.vercel.app](https://sequelize-guard.vercel.app))
- 🐛 **Bug Reports:** [GitHub Issues](https://github.com/lotivo/sequelize-guard/issues)
- 💬 **Questions:** [GitHub Discussions](https://github.com/lotivo/sequelize-guard/discussions)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
---
title: "5 Years Later: sequelize-guard Goes Full TypeScript (with AI as Co-Pilot)"
description: "The journey of rewriting a 5-year-old JavaScript authorization library in TypeScript, and how AI helped modernize an open-source project while preserving its battle-tested architecture."
date: 2024-12-11
author: Pankaj Vaghela
tags: ["typescript", "open-source", "system-design", "ai", "sequelize"]
---

# 5 Years Later: sequelize-guard Goes Full TypeScript (with AI as Co-Pilot)

Back in 2019, I open-sourced [sequelize-guard](https://github.com/lotivo/sequelize-guard)—a lightweight authorization library that brings Laravel-style permissions to Sequelize.js in Node.js. It was born out of a real need: managing role-based access control without drowning in complexity.

Today, I'm proud to ship **v6**: fully rewritten in TypeScript with a [modern documentation site](https://sequelize-guard.js.org)!

This post is about the journey, the lessons learned about system design, and how AI became an invaluable co-pilot in modernizing open-source software.

## The Genesis: Why sequelize-guard Exists

Authorization is one of those problems that seems simple until you actually implement it. You need roles, permissions, caching, audit trails, and a clean API that doesn't make developers want to tear their hair out.

I wanted something that felt as intuitive as Laravel's authorization system but worked seamlessly with Sequelize in Node.js. The result was sequelize-guard:

```typescript
await user.can('edit:post'); // Simple, readable, powerful
```

Clean separation of roles, permissions, and caching. Event-driven hooks for auditing. A fluent API that reads like natural language.

## System Design That Stands the Test of Time

Here's the thing about good architecture: it ages well.

The original JavaScript codebase was built with longevity in mind. Even after 5 years, multiple Sequelize major versions (v5 → v6), and the entire JavaScript ecosystem evolving around it, **the core logic ported to TypeScript almost unchanged**.

### What Made It Last?

**1. Strong Abstractions**

The library was built around clear concepts:
- **Roles** as containers for permissions
- **Permissions** as action-resource pairs
- **Caching layer** as a separate concern
- **Event system** for extensibility

These abstractions didn't leak. They didn't depend on Sequelize internals more than necessary. They just... worked.

**2. Fluent, Readable API**

```typescript
// This API design from 2019 still feels modern in 2024
user.can('edit:post')
user.isA('admin')
user.isAnyOf(['admin', 'editor'])
```

When your API reads like natural language, it doesn't age. It doesn't need "modernizing" because it was already intuitive.

**3. Event-Driven Architecture**

```typescript
guard.on('permission:checked', (event) => {
// Audit logging, analytics, whatever you need
});
```

By emitting events at key points, the library became extensible without modification. Users could hook into the authorization flow without touching the core.

**The Lesson**: Invest in strong abstractions early. They make migrations painless and keep your code relevant for years.

## Maintainability = Survival in Open Source

JavaScript evolves at breakneck speed. What was cutting-edge in 2019 became legacy by 2024.

The v6 rewrite wasn't just about adding types—it was about **ensuring the project's survival**:

### The Modernization Checklist

- ✅ **TypeScript with strict generics** for models & permissions
- ✅ **Dual ESM + CommonJS support** for maximum compatibility
- ✅ **Vitest** for testing (goodbye Jest config hell)
- ✅ **Vite** for blazing-fast builds
- ✅ **Typedoc** for auto-generated API documentation
- ✅ **99% backward compatible** (breaking changes only where absolutely necessary)

### Why This Matters

Open source isn't "set it and forget it." Projects that don't evolve become abandonware. Users move on. Contributors disappear. The ecosystem leaves you behind.

By modernizing the toolchain, I ensured that:
- New contributors can jump in with familiar tools
- The library works with modern Node.js projects (ESM)
- TypeScript users get first-class support
- The documentation stays fresh and accurate

**The Lesson**: Evolve or fade. Maintenance isn't glamorous, but it's what keeps projects alive.

## LLMs as Open-Source Superchargers

Here's where it gets interesting.

I approached this massive rewrite by treating Claude and GPT-4 as my **co-pilots**. I gave them the old JavaScript codebase and a clear task:

> "Migrate to strict TypeScript. Keep 100% behavior. Add generics. Update tests."

### What AI Crushed

**The Grunt Work:**
- Type definitions across hundreds of lines of code
- Converting modules from CommonJS to ESM
- Migrating test suites from Jest to Vitest
- Generating boilerplate for generic types
- Updating configuration files for modern tooling

This is the stuff that's tedious, error-prone, and soul-crushing. AI handled it with remarkable accuracy.

### What I Still Owned

**The High-Value Work:**
- Core architecture decisions
- Handling complex edge cases specific to Sequelize
- Designing the generic type system for models
- Setting up CI/CD pipelines
- Building and deploying the documentation site
- Reviewing and validating AI-generated code

### The Truth About AI-Assisted Development

**AI didn't replace me.** It removed the noise so I could focus purely on quality, correctness, and architecture.

Think of it like this:
- **Before AI**: 70% grunt work, 30% creative problem-solving
- **With AI**: 20% grunt work, 80% creative problem-solving

The work became more intellectually engaging. I spent less time typing boilerplate and more time thinking about design.

**The Lesson**: AI is a force multiplier for maintainers. It doesn't eliminate the need for expertise—it amplifies it.

## The Result: Battle-Tested, Type-Safe, Future-Proof

sequelize-guard v6 is everything the original was, but better:

```typescript
// Now with compile-time type checking!
await user.can('delete:user'); // TypeScript knows this is valid

// Generic support for your models
const guard = new SequelizeGuard<MyUserModel>(sequelize);

// Full IntelliSense support
user.assignRole('admin'); // Autocomplete works perfectly
```

### What Users Get

- **Type Safety**: Catch permission errors at compile time
- **Better DX**: IntelliSense, autocomplete, inline documentation
- **Modern Tooling**: Works with ESM, Vite, and modern Node.js
- **Same Great API**: If you used v5, v6 feels familiar
- **Comprehensive Docs**: [sequelize-guard.js.org](https://sequelize-guard.js.org)

## A Message to Open-Source Maintainers

If you have a dusty open-source gem sitting in your GitHub repos, **it's never too late to modernize**.

The tools are better than ever:
- TypeScript makes refactoring safer
- AI can help carry the grunt work
- Modern build tools (Vite, Vitest) are faster and simpler
- Documentation generators (Typedoc, VitePress) are incredible

The community is here. People still care about well-designed libraries that solve real problems.

Don't let your project fade because the tooling aged. Give it new life.

## Try It Out

If you use Sequelize + Node.js, I'd love for you to try sequelize-guard v6:

- 🌟 [Star on GitHub](https://github.com/lotivo/sequelize-guard)
- 📖 [Read the Docs](https://sequelize-guard.js.org)
- 📦 [Install from NPM](https://www.npmjs.com/package/sequelize-guard)

I'd especially love to hear your thoughts on the new type system and developer experience.

---

**About the Author**: Pankaj Vaghela is a software engineer passionate about system design, open source, and building tools that make developers' lives easier. With over a decade of experience in full-stack development, he specializes in creating scalable solutions and contributing to the open-source community. Learn more at [pankajvaghela.in](https://pankajvaghela.in/) or connect on [GitHub](https://github.com/pankajvaghela).
12 changes: 12 additions & 0 deletions apps/sequelize-guard-docs/source.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
frontmatterSchema,
metaSchema,
} from 'fumadocs-mdx/config';
import { z } from 'zod';

// You can customise Zod schemas for frontmatter and `meta.json` here
// see https://fumadocs.dev/docs/mdx/collections
Expand All @@ -20,6 +21,17 @@ export const docs = defineDocs({
},
});

export const blogs = defineDocs({
dir: 'content/blogs',
docs: {
schema: frontmatterSchema.extend({
author: z.string().optional(),
date: z.string().or(z.date()).optional(),
tags: z.array(z.string()).optional(),
}),
},
});

export default defineConfig({
mdxOptions: {
// MDX options
Expand Down
45 changes: 45 additions & 0 deletions apps/sequelize-guard-docs/src/app/blogs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { blogSource } from '@/lib/source';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { BlogIndexPage } from '../components/BlogIndexPage';
import { BlogPostPage } from '../components/BlogPostPage';

export default async function Page(props: {
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;

// If no slug, show blog index
if (!params.slug || params.slug.length === 0) {
return <BlogIndexPage />;
}

// Otherwise, show individual blog post
return <BlogPostPage slug={params.slug} />;
}

export async function generateStaticParams() {
return blogSource.generateParams();
}

export async function generateMetadata(props: {
params: Promise<{ slug?: string[] }>;
}): Promise<Metadata> {
const params = await props.params;

// If no slug, return blog index metadata
if (!params.slug || params.slug.length === 0) {
return {
title: 'Blog - Sequelize Guard',
description: 'Insights, updates, and stories about sequelize-guard',
};
}

const page = blogSource.getPage(params.slug);
if (!page) notFound();

return {
title: page.data.title,
description: page.data.description,
};
}
100 changes: 100 additions & 0 deletions apps/sequelize-guard-docs/src/app/blogs/components/BlogIndexPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { blogSource } from '@/lib/source';
import Link from 'next/link';
import { Calendar, User, Tag } from 'lucide-react';

export function BlogIndexPage() {
const allBlogs = [...blogSource.getPages()].sort((a, b) => {
const dateA = new Date(a.data.date || 0).getTime();
const dateB = new Date(b.data.date || 0).getTime();
return dateB - dateA;
});

return (
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
<div className="max-w-5xl mx-auto px-6 py-16">
<div className="mb-12">
<Link
href="/"
className="text-emerald-600 hover:text-emerald-700 font-medium mb-4 inline-block"
>
← Back to Home
</Link>
<h1 className="text-5xl font-bold text-slate-900 mb-4">Blog</h1>
<p className="text-xl text-slate-600">
Insights, updates, and stories about sequelize-guard
</p>
</div>

<div className="space-y-8">
{allBlogs.map((blog) => (
<article
key={blog.url}
className="bg-white rounded-xl shadow-md hover:shadow-lg transition-shadow p-8 border border-slate-200"
>
<Link href={blog.url} className="group">
<h2 className="text-3xl font-bold text-slate-900 mb-3 group-hover:text-emerald-600 transition">
{blog.data.title}
</h2>
</Link>

{blog.data.description && (
<p className="text-lg text-slate-600 mb-4">
{blog.data.description}
</p>
)}

<div className="flex flex-wrap gap-4 text-sm text-slate-500 mb-4">
{blog.data.date && (
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
<time>
{new Date(blog.data.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
</div>
)}

{blog.data.author && (
<div className="flex items-center gap-2">
<User className="w-4 h-4" />
<span>{blog.data.author}</span>
</div>
)}
</div>

{blog.data.tags && blog.data.tags.length > 0 && (
<div className="flex items-center gap-2 flex-wrap">
<Tag className="w-4 h-4 text-slate-400" />
{blog.data.tags.map((tag: string) => (
<span
key={tag}
className="px-3 py-1 bg-emerald-50 text-emerald-700 rounded-full text-sm font-medium"
>
{tag}
</span>
))}
</div>
)}

<Link
href={blog.url}
className="inline-flex items-center gap-2 mt-6 text-emerald-600 hover:text-emerald-700 font-semibold"
>
Read more →
</Link>
</article>
))}
</div>

{allBlogs.length === 0 && (
<div className="text-center py-16">
<p className="text-xl text-slate-500">No blog posts yet. Check back soon!</p>
</div>
)}
</div>
</div>
);
}
Loading