Stack logo
Bright ideas and techniques for building with Convex.
Profile image
Ian Macartney
2 months ago

The “full-stack framework” fallacy

an icon representing fullstack programming inside a paradoxical shape

I'd like to get into the nuance of Frontend vs. Backend vs. Full-stack, given all the buzz around Laravel and the JS ecosystem.

My take is that when people want the JS ecosystem to have a Ruby on Rails type of full-stack offering, what they really want is an opinionated backend ecosystem that interoperates seamlessly with their frontends in a way that empowers full-stack developers to easily build apps that can scale.

Opinionated backend

Having a backend platform that has opinionated answers for these is awesome:

  • Schema definition, especially if it guarantees that the schema in code matches the live DB schema, and enables managing the evolution of your schema alongside code versioning.
  • Database access: querying with indexes and pagination, writes with strong transactional guarantees, schema validation.
  • Argument validation so your code doesn’t have to opt-in to safety.
  • Authentication baked in.
  • Scheduled jobs, or whatever your preferred term is for functions that run in the background to support async workflows, crons, retries, etc.
  • Realtime subscriptions / streaming (e.g. using a Socket, WebSocket, or Server-Sent Events)
  • Caching with automatic invalidation and consistency guarantees.
  • Security: hiding data from frontends, authorizing access and execution, not just exposing your database to a client directly.
  • Text search that is consistent with the searched data.
  • Vector search for your hot new AI app idea.
  • File storage with strong durability guarantees.
  • Automatic scaling so you don’t get bogged down in DevOps.

Not having these in a consistent package is a huge pain point surfaced by this whole discussion, and highlights the fragmentation of the JS ecosystem. Beyond decision fatigue, or wiring together a constellation of frameworks, libraries and services, it’s cumbersome today for an ecosystem to develop. It’s not easy to:

  1. Publish an npm package with a frontend component that also needs to talk to a server function, which needs a consistent interface for accessing persisted data.
  2. Drop in a library that talks to your own DB with transactional guarantees that also calls 3rd party APIs, handles webhooks, and schedules future follow-up tasks. For instance, implementing a drip email campaign that has rules around sending emails to users, based on some state in the DB.

However, I’ll posit that this backend doesn't need to tightly couple with html rendering or your client-specific logic.

Frontend inter-op

Having a frontend framework that seamlessly works with your backend framework is amazing:

  • End-to-end type safety so you can author frontend and backend changes simultaneously.
  • Reactive / realtime updates so your UI automatically updates when data changes.
  • Optimistic updates for local changes that automatically resolve when the server data has updated and is reflected locally.
  • Authentication flows that enable automatically prompting the user to log in and securely hiding UI.
  • Consistent data views: all of the data on a page coming from the same logical snapshot, so you don’t have two parts of your UI that disagree.

When you don’t have these, you end up with workarounds:

  1. You separately declare the types that you expect from http endpoints, or rely on codegen which can get out of date.
  2. You poll for new data or expect users to “just refresh the tab” to get updated data.
  3. You hand-craft logic to re-fetch the right data whenever a user does something meaningful.

However, this doesn't need to be the same framework as your backend (and probably shouldn't if you care about multi-platform support).

The full-stack fallacy

Full-stack is exciting because the same developer can work on the frontend and backend at the same time, ideally in the same language so the same types flow through. Full-stack development is about enabling full-stack workflows.

It doesn't require that the same company makes your backend and frontend abstractions, only that there are strong abstractions that enable working across them transparently.

It's nice when you can have multiple frontends for your product co-exist, even if they use different frameworks or languages (ReactNative, React, Swift, Kotlin, etc.). It’s a benefit to be able to change frontends over time without rewriting all of your business logic, while still being owned by a single full-stack dev. Once you deeply couple your business logic with an html rendering strategy, what are you going to do when you decide you need a native mobile app?

The Convex take

With Convex as your backend you can write your backend in TypeScript and your frontend with Next.js, Vite (Remix), Vue, Svelte, or whatever comes next, and they can all talk to the same backend API with end-to-end types and real-time reactivity. This is possible because Convex has clients that provide types, manage the WebSocket connection, ensure consistent data views, refetch auth tokens, expose optimistic update APIs, and more. And if you’re not using TypeScript, you can still hit the same API endpoints from any language and get the same guarantees, whether you’re using our Rust client library (which powers our python client) or talking HTTP directly. You can still share this logic with your server-side React calls, but there is a clear distinction between your frontend logic and backend API.

Convex has built-in:

  • Schema validation with atomic code/schema versioning.
  • Reactive database with the strongest transaction guarantees (ACID with serializable isolation): no more read-modify-write races.
  • Realtime subscriptions over WebSocket with zero-config consistent caching with automatic cache invalidation.
  • Argument validation so your code doesn’t have to opt-in to safety or cast types.
  • Authentication to know which user is making a request.
  • Scheduled jobs that can be transactionally scheduled and canceled (separate from cron jobs, which it also has).
  • Security: hiding data from frontends using server-side functions, authorizing access and execution, not just exposing your database to a client directly with an over-wrought RLS DSL.
  • Text search that is immediately consistent with the searched data.
  • Vector search for your hot new AI app idea.
  • File storage with strong durability guarantees.
  • Automatic scaling by the team that built and scaled Dropbox.

This enables you to ship frontend components that have associated backend logic, and makes it easy for people to drop that into their own projects, since the database models and logic is all speaking Convex, rather than supporting any number of database connectors with varying degrees of transaction guarantees (Convex has serializable isolation - the highest level).

It's the opinionated backend for TypeScript developers AND you can hit the same API from GoLang / Swift / etc. without duplicating all your business logic from your /app to /api.

I made a stateful migration management tool that is built entirely on top of Convex (anyone could have written it) and published it along with many other tools to the convex-helpers npm package. It calls user-defined functions, saves migrations state, recursively schedules batches of data to migrate, transactionally reads and writes to your database, and is trivial to drop into any Convex project. I also just published a rate limiting library that provides transactional application-layer rate limiting. It adds a single table added to the user’s Convex project, and all the operations happen within the same transaction window as the code using it.

We are also in the midst of designing and building a “components” ecosystem for backend logic - so you can easily extend your own app with opinionated, stateful, powerful backend features like email sending or Stripe payment handling. It’s like spinning up a constellation of services in Kubernetes, except you get atomic deploys, cross-component transactions, type safe APIs, and strongly isolated function calls instead of network RPCs and service-oriented version skew hell. And you don’t have to use Kubernetes (which I personally loved nerding out on, but gets in the way of actually building a product people care about).

Summary

Full-stack is great and here to stay. Writing your frontend and backend in the same language is really useful. Opinionated backends should make your life easier, and not box you into coupling your data and business logic with your web app UI. Full-stack workflows should enable you to seamlessly co-develop your frontend and backend with end-to-end typing, strong data correctness guarantees, caching, and all of the tools you need to build a modern responsive app.