diff --git a/src/mapeo-manager.js b/src/mapeo-manager.js
index 71fb2684..e9539af9 100644
--- a/src/mapeo-manager.js
+++ b/src/mapeo-manager.js
@@ -95,6 +95,7 @@ export const DEFAULT_ONLINE_STYLE_URL =
 export class MapeoManager extends TypedEmitter {
   #keyManager
   #projectSettingsIndexWriter
+  #sqlite
   #db
   // Maps project public id -> project instance
   /** @type {Map<string, MapeoProject>} */
@@ -146,12 +147,12 @@ export class MapeoManager extends TypedEmitter {
     this.#l = Logger.create('manager', logger)
     this.#dbFolder = dbFolder
     this.#projectMigrationsFolder = projectMigrationsFolder
-    const sqlite = new Database(
+    this.#sqlite = new Database(
       dbFolder === ':memory:'
         ? ':memory:'
         : path.join(dbFolder, CLIENT_SQLITE_FILE_NAME)
     )
-    this.#db = drizzle(sqlite)
+    this.#db = drizzle(this.#sqlite)
     migrate(this.#db, { migrationsFolder: clientMigrationsFolder })
 
     this.#localPeers = new LocalPeers({ logger })
@@ -166,7 +167,7 @@ export class MapeoManager extends TypedEmitter {
 
     this.#projectSettingsIndexWriter = new IndexWriter({
       tables: [projectSettingsTable],
-      sqlite,
+      sqlite: this.#sqlite,
       logger,
     })
     this.#activeProjects = new Map()
@@ -932,6 +933,15 @@ export class MapeoManager extends TypedEmitter {
     await pTimeout(this.#fastify.ready(), { milliseconds: 1000 })
     return (await this.#getMediaBaseUrl('maps')) + '/style.json'
   }
+
+  async close() {
+    /** @type {Promise<unknown>[]} */ const promises = []
+    for (const project of this.#activeProjects.values()) {
+      promises.push(project.close())
+    }
+    await Promise.all(promises)
+    this.#sqlite.close()
+  }
 }
 
 // We use the `protomux` property of connected peers internally, but we don't
diff --git a/test-e2e/migration.js b/test-e2e/migration.js
index aa16b946..e86fc7f9 100644
--- a/test-e2e/migration.js
+++ b/test-e2e/migration.js
@@ -131,7 +131,7 @@ test('migration of localDeviceInfo table', async (t) => {
     expectedDeviceInfo
   )
 
-  // No manager.close() function yet, but should be ok
+  // No manager.close() function on old versions, but should be okay
 
   const manager = new MapeoManager({
     rootKey,
diff --git a/test-e2e/utils.js b/test-e2e/utils.js
index d07fdf8d..39f38f87 100644
--- a/test-e2e/utils.js
+++ b/test-e2e/utils.js
@@ -13,7 +13,7 @@ import { MapeoManager as MapeoManager_2_0_1 } from '@comapeo/core2.0.1'
 
 import { MapeoManager, roles } from '../src/index.js'
 import { generate } from '@mapeo/mock-data'
-import { valueOf } from '../src/utils.js'
+import { noop, valueOf } from '../src/utils.js'
 import { randomBytes, randomInt } from 'node:crypto'
 import { temporaryFile, temporaryDirectory } from 'tempy'
 import fsPromises from 'node:fs/promises'
@@ -245,19 +245,21 @@ export function createManager(seed, t, overrides = {}) {
     const directories = [temporaryDirectory(), temporaryDirectory()]
     ;[dbFolder, coreStorage] = directories
     t.after(() =>
-      Promise.all(
-        directories.map((dir) =>
-          fsPromises.rm(dir, {
-            recursive: true,
-            force: true,
-            maxRetries: 2,
-          })
+      fireAndForgetPromise(
+        Promise.all(
+          directories.map((dir) =>
+            fsPromises.rm(dir, {
+              recursive: true,
+              force: true,
+              maxRetries: 2,
+            })
+          )
         )
       )
     )
   }
 
-  return new MapeoManager({
+  const result = new MapeoManager({
     rootKey: getRootKey(seed),
     projectMigrationsFolder,
     clientMigrationsFolder,
@@ -266,6 +268,10 @@ export function createManager(seed, t, overrides = {}) {
     fastify: Fastify(),
     ...overrides,
   })
+
+  t.after(() => fireAndForgetPromise(result.close()))
+
+  return result
 }
 /**
  * @param {string} seed
@@ -479,6 +485,14 @@ function getRootKey(seed) {
   return key
 }
 
+/**
+ * @param {Promise<unknown>} promise
+ * @returns {Promise<void>}
+ */
+function fireAndForgetPromise(promise) {
+  return promise.then(noop).catch(noop)
+}
+
 /**
  *
  * @param {number} value