Разграничение данных между пользователями в приложениях, использующих IndexedDB и Dexie.js, строится вокруг принципа логического разделения записей внутри одной базы данных. В отличие от серверных систем, здесь отсутствует встроенный механизм строгой изоляции пользователей, поэтому вся модель безопасности реализуется на уровне структуры данных, индексов и правил доступа в коде приложения.
Основной подход к разделению данных в Dexie.js заключается в
добавлении идентификатора владельца ко всем сущностям, которые подлежат
разграничению. Обычно используется поле userId,
tenantId или аналогичный ключ, определяющий принадлежность
записи.
const db = new Dexie("appDatabase");
db.version(1).stores({
notes: "++id, userId, title, createdAt",
});
В такой схеме каждая запись привязывается к конкретному пользователю, а доступ к данным ограничивается фильтрацией по этому полю.
const currentUserId = "user_123";
const userNotes = await db.notes
.where("userId")
.equals(currentUserId)
.toArray();
Подобный подход формирует логическую «зону доступа» внутри одной таблицы, где границы определяются не самой базой данных, а дисциплиной запросов.
При работе с разграничением данных критически важным становится правильное проектирование индексов. Поле идентификатора пользователя должно быть индексировано, иначе фильтрация приведёт к полному сканированию таблицы.
Dexie.js позволяет комбинировать составные индексы для повышения эффективности запросов:
db.version(2).stores({
notes: "++id, [userId+createdAt], userId, createdAt, title",
});
Использование составного индекса [userId+createdAt]
позволяет эффективно выполнять выборки, ограниченные пользователем и
диапазоном времени:
const recentNotes = await db.notes
.where("[userId+createdAt]")
.between(
[currentUserId, Dexie.minKey],
[currentUserId, Dexie.maxKey]
)
.toArray();
Такой подход формирует естественную сегментацию данных внутри одного хранилища без необходимости физического разделения таблиц.
В приложениях с несколькими организациями или рабочими пространствами
используется расширенная модель, основанная на tenantId. В
этом случае каждая запись принадлежит не только пользователю, но и
пространству данных.
db.version(3).stores({
projects: "++id, tenantId, userId, name, updatedAt",
});
Запросы строятся с обязательным учетом обоих уровней изоляции:
const tenantProjects = await db.projects
.where("[tenantId+updatedAt]")
.between(
[currentTenantId, Dexie.minKey],
[currentTenantId, Dexie.maxKey]
)
.toArray();
Модель тенантов позволяет реализовывать сценарии совместной работы, сохраняя логическое разделение контекстов.
Прямое использование запросов к Dexie.js приводит к риску нарушения границ доступа. Для устранения этой проблемы вводится слой репозитория, который инкапсулирует правила фильтрации.
class NotesRepository {
constructor(db, getUserId) {
this.db = db;
this.getUserId = getUserId;
}
async getAll() {
return this.db.notes
.where("userId")
.equals(this.getUserId())
.toArray();
}
async add(note) {
return this.db.notes.add({
...note,
userId: this.getUserId(),
createdAt: Date.now(),
});
}
}
Такой слой формирует централизованную точку контроля, где закрепляется правило обязательного добавления идентификатора владельца.
Dexie.js предоставляет механизм lifecycle hooks, который позволяет принудительно внедрять ограничения на уровне операций записи.
db.notes.hook("creating", (primKey, obj) => {
obj.userId = currentUserId;
obj.createdAt = Date.now();
});
Хуки позволяют устранить класс ошибок, связанных с пропуском поля идентификатора пользователя на уровне бизнес-логики.
Дополнительно можно реализовать проверку целостности при обновлении:
db.notes.hook("updating", (mods, primKey, obj) => {
if (obj.userId !== currentUserId) {
throw new Error("Недопустимое изменение зоны доступа");
}
});
Таким образом обеспечивается защита от случайного или некорректного изменения границ данных.
Альтернативный подход к изоляции заключается в создании отдельной базы Dexie.js для каждого пользователя. Имя базы формируется динамически:
function createUserDb(userId) {
const db = new Dexie(`app_${userId}`);
db.version(1).stores({
notes: "++id, title, createdAt",
});
return db;
}
Этот метод обеспечивает физическое разделение данных, однако усложняет миграции схемы и управление несколькими базами одновременно. Также увеличивается нагрузка на инфраструктуру приложения при большом количестве пользователей.
Разграничение данных в Dexie.js носит исключительно прикладной характер. Любая логика, реализованная в браузере, не может считаться защитой от злоумышленника, имеющего доступ к среде выполнения.
Основные уязвимости модели:
userId в запросах;По этой причине зона доступа в Dexie.js рассматривается как средство организации данных, а не как механизм безопасности.
При реализации офлайн-режимов часто используется кэширование данных нескольких пользователей в одной базе. В таких случаях критически важно очищать состояние при смене пользователя.
async function switchUser(newUserId) {
await db.transaction("rw", db.notes, async () => {
await db.notes.clear();
});
currentUserId = newUserId;
}
Игнорирование очистки приводит к ситуации, когда данные разных пользователей сосуществуют в одной области хранения, что увеличивает риск некорректного отображения информации.
В сложных приложениях фильтрация данных строится на комбинации нескольких параметров: пользователь, роль, статус доступа, уровень видимости.
db.version(4).stores({
documents: "++id, userId, visibility, role, updatedAt",
});
Пример выборки данных с учетом зоны доступа и уровня видимости:
const accessibleDocs = await db.documents
.where("userId")
.equals(currentUserId)
.and(doc => doc.visibility !== "private")
.toArray();
Такая модель формирует многоуровневую фильтрацию, где зона доступа пересекается с бизнес-логикой приложения.
Изменение структуры таблиц может влиять на логику изоляции. Добавление новых индексов требует пересмотра стратегии хранения идентификаторов пользователей.
db.version(5).stores({
notes: "++id, userId, workspaceId, createdAt, updatedAt",
}).upgrade(tx => {
return tx.table("notes").toCollection().modify(note => {
if (!note.workspaceId) {
note.workspaceId = "default";
}
});
});
Миграции должны учитывать сохранение целостности зон доступа, иначе возможны ситуации смешивания данных разных контекстов.
Для более сложных сценариев вводится система ролей, которая влияет на доступ к данным внутри одной зоны.
db.version(6).stores({
tasks: "++id, userId, role, status, createdAt",
});
Фильтрация с учетом роли:
const tasks = await db.tasks
.where("userId")
.equals(currentUserId)
.filter(task => task.role !== "guest")
.toArray();
Ролевая модель дополняет изоляцию данных, добавляя поведенческий уровень контроля.
В приложениях с синхронизацией через сервер Dexie.js часто используется гибридная модель, где локальная зона доступа дублируется серверной политикой.
Локальный слой отвечает за организацию данных, серверный — за реальную безопасность и авторизацию. Несоответствие между ними приводит к рассинхронизации правил доступа, что требует строгого согласования схемы идентификаторов и политик фильтрации.
При большом объеме данных фильтрация по userId может
становиться узким местом. Использование индексов и предварительной
агрегации позволяет снизить нагрузку:
const stream = db.notes
.where("[userId+createdAt]")
.between([currentUserId, 0], [currentUserId, Date.now()])
.reverse()
.limit(50);
Такая конструкция обеспечивает ограничение зоны доступа уже на уровне индекса, минимизируя объем обрабатываемых данных.
На практике выделяются устойчивые подходы к проектированию:
userId во все таблицы с
пользовательскими данными;Такая комбинация формирует устойчивую архитектуру, в которой разграничение данных становится свойством структуры приложения, а не отдельных запросов.