DEV Community

Prithvi
Prithvi

Posted on

Building a Lightweight SaaS Application With Bun and SQLite

When building a SaaS application, it's easy to assume that a production-ready system needs a large infrastructure stack from day one.

Multiple databases, caches, queues, containers, microservices, and external services can all have their place. But adding complexity before it's actually needed can make a project harder to develop, deploy, and maintain.

While building CampusTPO, we took a more lightweight approach.

Keeping the Infrastructure Simple

CampusTPO is built around Bun and SQLite, with a server-rendered application architecture.

One of the reasons for choosing SQLite is straightforward: it provides a capable relational database without requiring a separate database server to manage.

For an application that doesn't yet require distributed database infrastructure, this can significantly simplify development and deployment.

SQLite's WAL mode also provides a practical way to handle concurrent reads and writes while keeping the database architecture relatively simple.

The philosophy is not that SQLite is the right choice for every application.

It's that the database should match the actual requirements of the application.

Authentication as a Core Feature

Authentication is one of those areas that is easy to underestimate when starting a SaaS project.

It's not just a login form.

A complete authentication system needs to consider account creation, verification, sessions, password recovery, and account management.

CampusTPO includes:

Email and password authentication
Email verification
Google sign-in
Password reset
Session management
Account management

Treating authentication as part of the application's foundation makes it easier to build the rest of the system around authenticated users and permissions.

Security From the Beginning

Security was another consideration throughout the architecture.

The application incorporates several security mechanisms, including:

Role-based access control (RBAC)
CSRF protection
Content Security Policy (CSP)
Rate limiting
Password hashing
Audit trails

These aren't independent checkboxes that automatically make an application secure.

They work together as layers.

For example, RBAC determines what an authenticated user is allowed to access, while CSRF protection helps defend state-changing requests against forged requests. CSP provides another layer against certain classes of browser-based attacks.

The important part is considering these controls during development rather than trying to bolt them onto the application later.

Keeping the Codebase Modular

Another design decision was keeping the application's responsibilities separated.

A simplified representation looks like:

Routes

Services

Repositories

SQLite

The route layer handles incoming requests.

The service layer contains application and business logic.

The repository layer handles data access.

The database remains behind that abstraction instead of being accessed directly throughout the application.

This separation isn't about creating as many layers as possible.

It's about giving different responsibilities clear boundaries.

As the project grows, that makes it easier to test individual components, change implementation details, and understand where particular functionality belongs.

Why Not Use More Infrastructure?

This has probably been one of the more interesting lessons from the project.

There's a tendency in modern application development to assume that a "serious" application needs a large technology stack.

Redis.

Message queues.

Kubernetes.

Multiple database systems.

Microservices.

Distributed workers.

Sometimes those technologies are exactly what a product needs.

But sometimes they aren't.

If the application doesn't currently have a problem that requires a queue, adding one doesn't necessarily improve the architecture.

It can instead introduce:

More services to deploy
More configuration
More failure points
More monitoring requirements
More maintenance
More things developers need to understand

A smaller architecture can actually be the better engineering decision when it matches the current requirements.

Building for Evolution

Keeping things simple doesn't mean designing an application that can never grow.

Quite the opposite.

A good starting architecture should leave room for change.

For example, if application requirements eventually justify moving away from SQLite, having database access isolated behind repositories makes that transition easier.

If background processing becomes necessary, a queue can be introduced when there's an actual workload that benefits from it.

If traffic increases significantly, individual components can be optimized or replaced based on measurable bottlenecks.

The key is to let the application's requirements drive those decisions.

The Bigger Lesson

One of the biggest lessons from building CampusTPO has been that architecture is about trade-offs.

There isn't a universally "best" technology stack.

A distributed architecture might be appropriate for one product and completely unnecessary for another.

A PostgreSQL cluster might be essential for one application while SQLite is perfectly reasonable for another.

The right question isn't:

"What technologies do large applications use?"

It's:

"What does this application actually need?"

That's the question we're trying to keep in mind while developing CampusTPO.

The goal is a foundation that is simple enough to understand, secure enough to trust, modular enough to maintain, and flexible enough to evolve.

And sometimes, that's a better starting point than building for a scale you don't have yet.

Top comments (0)