Workspace Scoping
Overview
Section titled “Overview”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.
Implementation
Section titled “Implementation”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.
Example Flow:
Section titled “Example Flow:”-
Middleware (
internal/middleware/auth.go): Theworkspace_idis 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) -
Handler (
internal/outlet/handler.go): The handler retrieves theworkspace_idfrom the context and passes it to the service layer.workspaceID := c.GetUint("workspace_id")outlets, meta, err := h.outletService.Find(c, &filter, workspaceID) -
Repository (
internal/repository/outlet_repository.go): The repository method receives theworkspaceIDand applies it as aWHEREclause in the GORM query.func (r *outletRepository) Find(db *gorm.DB, filter *domain.OutletListFilter, workspaceID uint) ([]domain.Outlet, *domain.PaginationMeta, error) {var outlets []domain.Outletquery := db.Model(&domain.Outlet{}).Where("workspace_id = ?", workspaceID)// ... apply other filters ...err := query.Find(&outlets).Errorreturn outlets, meta, err}