Skip to content

Workspace Scoping

Merq is a multi-tenant platform where all data is partitioned by a workspace_id. This ensures that users within one workspace cannot access data from another. This separation is fundamental to the platform’s security and data integrity.

Unlike systems that use automated scoping or database-level policies, Merq’s workspace scoping is enforced manually at the application level within the backend’s repository layer.

Data isolation is achieved by explicitly adding a .Where("workspace_id = ?", workspaceID) clause to every GORM query that accesses tenant-specific data.

The workspace_id is extracted from the user’s JWT token in the auth middleware, placed into the Gin context, and then passed down through the service layer to the repository layer, where it is applied to database queries.

  1. Middleware (internal/middleware/auth.go): The workspace_id is parsed from the JWT claims and attached to the request context.

    claims, err := utils.ParseJWT(tokenString)
    if err != nil {
    // ... error handling
    }
    c.Set("workspace_id", claims.WorkspaceID)
  2. Handler (internal/outlet/handler.go): The handler retrieves the workspace_id from the context and passes it to the service layer.

    workspaceID := c.GetUint("workspace_id")
    outlets, meta, err := h.outletService.Find(c, &filter, workspaceID)
  3. Repository (internal/repository/outlet_repository.go): The repository method receives the workspaceID and applies it as a WHERE clause in the GORM query.

    func (r *outletRepository) Find(db *gorm.DB, filter *domain.OutletListFilter, workspaceID uint) ([]domain.Outlet, *domain.PaginationMeta, error) {
    var outlets []domain.Outlet
    query := db.Model(&domain.Outlet{}).Where("workspace_id = ?", workspaceID)
    // ... apply other filters ...
    err := query.Find(&outlets).Error
    return outlets, meta, err
    }