Running an Access Database on a Modern Stack Without Rewriting It
The default advice for an Access application is rewrite it. Move the tables to a real database, replace the forms with a web front end, and port the VBA to Python or C#. It is clean, it is what everyone suggests, and for a small business it is usually the wrong first move — because the rewrite re-implements the one thing you cannot afford to get wrong, which is the business logic that took twenty years to accumulate and now lives in someone's macros.
There is a second option that is less talked about because it is less dramatic: keep the front end, replace only the weak part, and add a layer where you need reach. An Access "application" is really two things glued together — a file database (the Jet engine and the .accdb), and the UI plus VBA that sits on top. The file database is the weak link: one file, weak concurrency, no real backup or high availability, and a driver model that stops scaling around a dozen users. The UI and the VBA are the asset. So you do not have to rewrite the asset to fix the weak part.
This article is the playbook for that: three moves you can make in escalating order, each of which leaves the Access forms and VBA you have intact, plus the honest limits of each — because "without rewriting" has a ceiling, and you should know where it is before you commit.
First: name the two halves of your Access system
Almost every "Access app" you will find is a single .accdb or .mdb file that contains all of this at once:
| Half | What is in it | Is it the weak part or the asset? |
|---|---|---|
| The file database (Jet engine) | Tables, indexes, relationships — the data store itself | Weak part. Single file, file-lock based, limited concurrent writers, hard to back up safely while in use |
| The front end + logic | Forms, reports, VBA modules, macros, the AutoExec startup chain | Asset. This is where two decades of business rules live. Rewriting it is where rewrites go to die |
"Modernize without rewriting" means: leave the asset alone, and do all of your work on the weak part and in the layer above it. That single re-framing is what makes the whole approach work, and it is why you can keep the exact forms your staff already know how to use.
You do not have to do all three. Move 0 is a day or two and removes the biggest risk (a database living in someone's Downloads folder). Move 1 is where the real modernization happens. Move 2 is only for when you need reach that a desktop app cannot give you. Pick the highest move you can actually complete — an abandoned Move 2 is worse than a finished Move 1.
Move 0 — Harden in place (do this first, no matter what)
Before any architecture, fix the fact that the database is a file in an unmanaged location. This changes no logic and rewrites nothing, but it is the difference between "we can restore it" and "we found out on the 14th that we can't."
- Split the database if it is not split. Front end (forms, reports, VBA) in one
.accdb; back end (tables) in a second.accdbon a server, with the front end holding linked tables to it. A single all-in-one file that ten people open over a network is the classic Access failure. Splitting is a 30-minute job in the Access UI, not a project. - Put the back end on one proper machine, not on every desktop. The tables live in one place. The front ends are thin copies that anyone can open. That is the whole concurrency story of a well-run Access system.
- Make the backup real and test the restore. A copy of a file that is open is not a backup. You need a closed-database copy, run on a schedule, and — the part everyone skips — a restore that has actually been run once. I cover this in depth in Backing Up Microsoft Access: What Actually Works.
- Compact and repair on a schedule, and watch the file size. A slowly growing
.accdbis a known Jet symptom. If it is growing every week, you have a problem hiding behind a problem. - Pin the environment. Write down the Office/Jet bitness (32- vs 64-bit), the Office version, and any add-ins the VBA depends on. Access is only as portable as its host, and this list is your portability spec.
Move 0 does not make the system scalable. It makes it recoverable, and it gives you the stable base that Moves 1 and 2 are built on. If you do nothing else this year, do this.
Move 1 — Replace the file with a managed database, keep the front end
This is the move that most people skip because they assume it requires a rewrite. It does not, as long as your front end talks to its data through tables rather than through a hard-coded file path. And that is exactly what a split database gives you: the Access forms read and write tables, and a table does not care whether it lives in a local Jet file or in PostgreSQL or SQL Server — as long as the front end is linked to the right place.
The mechanics:
- Export the schema. Read your table definitions — names, field types, indexes, relationships, and any computed fields. Access will generate the DDL for you, or you can read it from the Jet catalog. You are not writing this by hand; you are capturing it.
- Import into a managed database. Create the same tables in PostgreSQL or SQL Server (whichever you will actually operate — both are fine, and neither is the only option). Import the data. At this point you have a managed, backed-up, high-availability copy of your data with the same shape.
- Re-point the front end. In the Access front end, remove the old linked tables and link to the new database instead. For most tables this is Database Tools → Linked Table Manager → change the source. The forms, reports, and VBA do not know — and do not need to know — where the rows come from.
- Test against pinned data, not against memory. Take a known snapshot of the old back end. Run the front end against the new back end. Compare. The numbers must be identical, because the logic did not change — only the storage did. If they differ, you found a data-type mapping bug, and finding it now is free compared to finding it in production.
- Run old and new in parallel for one real cycle. The old file keeps being the system of record. The new one runs alongside. Diff quietly. Then cut over, and keep the old file warm for 90 days exactly as I do in every other migration on this site.
After Move 1, you have done something genuinely important without touching a line of VBA: your data now lives on a database that has real backups, real concurrency, real restore points, and does not depend on a single file not being corrupted. Your staff still open the same forms. The machine that used to host a fragile file now hosts a managed service.
One honest caveat, because people trip on it: Move 1 changes the storage engine, not the concurrency model of the front ends. If your real problem was "thirty people writing at the same time," a managed back end helps a lot — it handles the writes correctly now — but it does not turn a form-based desktop app into a web app. That is Move 2's job. Do not let Move 1 be sold to you as if it solves reach.
Move 2 — Add a service layer on top (reach, without touching the core)
This is the move you only do when you need it: when someone has to reach the system from a browser, a phone, or another system, and a desktop Access window will not do. The key idea is add a thin layer above the logic — do not rewrite the logic. The Access app (or the managed database it reads from) becomes a service that something else calls.
One boundary, said out loud, because it is where this article meets the VB6 report article: if what you need to reach is a report — a query plus a layout — it can usually be expressed in SQL, and the cleaner path is a two-layer rebuild (a SQL data layer plus a presentation layer), not a COM call. COM-wrapping the VBA is for logic that lives inside VBA and is not a query — the pricing rule, the posting logic, the thing you would otherwise have to rewrite. Same goal (reach, without rewriting), different door. Pick the door by where the value actually sits: in the data, or in the code.
There are two honest ways to do this, and they differ in what they can reach:
| Wrapper style | What it calls | Good for | Watch out for |
|---|---|---|---|
| Read the managed DB directly | The PostgreSQL/SQL Server from Move 1 | Any query or read that is expressed as SQL — reports, lookups, sync to another system | It bypasses the VBA. If a "value" is computed inside a VBA function, the raw DB row is not the same as what the form shows |
| Call the VBA via COM | The Access app itself, through the COM automation interface | Logic that lives in VBA and you are not touching — the app is now a callable service | The host is a Windows machine with Access installed; the call is synchronous and single-threaded, so it is a service, not a web server |
The COM route is the one that genuinely "doesn't rewrite" anything, because it treats your Access app as a black box with a function in and a value out. A minimal Python call looks like this:
Two things to be honest about with Move 2. First, Access and VBA are Windows-only, so a COM wrapper has to run on a Windows host — a Windows managed instance is the natural fit (a Cloudways-managed Windows instance ↗ gives you patching, backups, and monitoring without owning the box). Affiliate link — I may earn a commission if you sign up; you pay the same price either way. Do not plan to run Access on a cheap Linux box; it will not, and the plan is wrong, not the hardware. Second, the COM wrapper is a service, not a web framework. It is single-threaded and synchronous. It is perfect for "call the pricing logic from another internal system," and it is not the right thing for "serve 200 customers a checkout page." For that you need a real application, and that is a rewrite — which is the ceiling "without rewriting" runs into.
Where "without rewriting" stops
Say the limits out loud, because a plan that ignores them fails quietly:
- It does not scale the UI. Forms are desktop-shaped. Move 1 fixes the data; it does not give you a browser. Move 2 gives you reach into the logic, not a product-grade web app.
- It does not fix logic that is itself the problem. If the reason you want to modernize is that the VBA is a 4,000-line tangle nobody trusts, wrapping it in a service wraps the tangle. Wrapping is not refactoring.
- It is bounded by the Jet/ODBC boundary. Field types, lengths, and behaviors differ between Jet and a managed RDBMS. The data-layer test in Move 1 exists to catch exactly this — do not skip it.
- It leaves you on Windows. Every layer that touches Access is Windows-only. That is a real operating constraint, not a detail.
None of these make the approach wrong — they make it scoped. "Without rewriting" is a legitimate strategy with a known ceiling, and knowing the ceiling is what makes it a strategy instead of a hope.
Risks that actually bite, and the cheap mitigations
| Risk | How it shows up | Cheap mitigation |
|---|---|---|
| Concurrency illusion | Move 1 done, yet "two people still can't save at once" | Split the database first (Move 0). The back end must be one file/DB; the front ends are the many |
| Bitness mismatch | "Linked tables work on my PC, not on the server" — 32-bit Office pointing at a 64-bit driver | Pin the Office/Jet bitness in Move 0 and install the matching ODBC/Jet driver on the host |
| Data-type drift on import | A "Memo" field, a Currency rounding, or a Date behaves differently in PostgreSQL/SQL Server | Compare front-end output to the pinned snapshot (Move 1, step 4). Fix the mapping, not the logic |
| Hidden startup dependencies | An AutoExec macro or startup form that only ran because of a local path or a local network name | Inventory the startup chain before you move; re-create the dependency on the new host |
| Wrapper exposes what the form hid | Move 2's service gets called more than the desktop app ever was, and a slow VBA path now blocks a caller | Treat the COM service as single-threaded; add timeouts; keep it an internal service, not a public one |
| The "it works on one machine" trap | Everything fine until the second machine, or the first reboot | Move 0 is specifically about making the environment portable and documented. Do not skip it to save a day |
How to choose — the honest decision
| Your situation | Do |
|---|---|
| Access file on a desktop, no real backup, 1–5 users | Move 0 only. That may be the whole job. Do not over-build. |
| 5–20 users, shared back end, you want real backup/HA and to get off a fragile file | Move 0 + Move 1. The sweet spot for most small businesses. Front end untouched. |
| You also need web/phone/reach or to feed another system | Move 0 + 1 + 2. And only then — Move 2 needs Moves 0 and 1 underneath it to be stable |
| The VBA itself is the problem (huge, untrusted, the real reason for the project) | Wrapping will not fix this. That is a rewrite of the logic, scoped carefully — a different project, and I would plan it as one |
FAQ
Will my staff notice anything?
For Move 0 and Move 1, ideally nothing: they open the same forms, run the same reports, and the numbers are identical. The changes are underneath — where the data lives and how it is backed up. Move 2 is the one that adds new capability (reach from a browser or another system), and that is a new thing, not a change to the old one.
Can I do Move 1 without stopping the business?
You do the build on the side, then cut over at a boundary. The old back end stays live and is the system of record until the new one has run one full real cycle in parallel and matched. There is no moment where the business is "down" waiting for a rewrite — that is the point of doing this underneath a system that keeps running.
Which managed database — PostgreSQL or SQL Server?
Both work for this. Choose by what you will actually operate and what your data is closest to. If your Access app leans on SQL-Server-isms or your team already runs SQL Server, SQL Server is the path of least resistance. If you want open-source, portability, and lower cost, PostgreSQL is strong. The front end does not care; you do, so pick on operating grounds, not on which is "more modern."
Why can't the wrapper run on a cheap Linux server?
Because the thing it wraps — Access and VBA — only runs on Windows. A Python service that reads a managed database could run anywhere, but the moment it calls the VBA via COM, it needs a Windows host with Access installed. Plan for that, or use the read-the-DB style for the parts that are pure SQL.
Is this a replacement for the "rewrite it" people?
It is the alternative for the common case, where the logic is sound and the risk is in the file and the reach. If your real problem is the logic, then yes — you are doing a rewrite, and this article is not it. Read Retiring a VB6/VBA Application and Microsoft Access Alternatives to compare the full-rewrite option honestly.
Summary
An Access application is two halves: a file database that is weak, and a front end plus VBA that is the asset. "Modernize without rewriting" is not a slogan — it is the discipline of doing all your work on the weak half and in the layer above it, and leaving the asset alone. Move 0 makes it recoverable (split it, back it up, test the restore). Move 1 replaces the fragile file with a managed database and re-points the same front end to it — your staff see no change, but the data now has real backups and real concurrency. Move 2 adds a thin service on top for reach, calling the VBA through COM on a Windows host — and it has a ceiling, because the UI is still desktop-shaped and the host is still Windows. Know that ceiling, and "without rewriting" stops being a hope and becomes a plan with a known edge.
Where to go next
- Your Access backup is a copy of a file that was open? → Backing Up Microsoft Access: What Actually Works
- Deciding between modernizing in place and full replacement? → Microsoft Access Alternatives: A 2026 Comparison
- The app is VB6/VBA rather than Access? → Retiring a VB6/VBA Application: The 2026 Playbook
- The reports are the part that must not break? → Migrating a VB6 Report to a Modern Stack
- Not sure what this is really costing you before you commit? → The Hidden Cost of Keeping a Legacy System Alive