Repository: skybridgeskills/dcc-admin-dashboard
Pull request #4: Multi-Tenancy Data Isolation
Author: login: nqthqn
Created: 2025-04-01T20:47:55Z | Merged: 2025-04-22T21:58:10Z

This adds data isolation in support of multi-tenancy.

- new `tenant` text field on all collections
- User collection has `tenant` and `isAdmin` saved to JWT
- `server.ts` middleware checks for mismatch on every request
- collection `hooks` and `access` has been updated to use the new fields

To test, add some lines to `/etc/hosts`

```
127.0.0.1	localhost
127.0.0.1   t1.localhost
127.0.0.1   t2.localhost
```

Things I have verified:

- If admin creates user on `t1.localhost:3000`  then that user shall not be able to login via `t2.localhost:3000`
- Can create initial admin user when no users in collection
- All data is scoped to `req.headers.host` (current tenant domain)
- All data that is created automatically gets tenant set to `req.headers.host`
- Only admins can list and create users (for current tenant domain)
- Regular users can edit their own user


## Upgrading

A `tenant` string property needs to be set on every object in every collection and a user needs to have `isAdmin` boolean set to true. Like this:

```js
db.yourCollection.updateMany(
  {},
  { $set: { tenant: "localhost" } }
)
```


Diff:
diff --git a/src/collections/CredentialBatches.ts b/src/collections/CredentialBatches.ts
index 4d21dfd..a6e0294 100644
--- a/src/collections/CredentialBatches.ts
+++ b/src/collections/CredentialBatches.ts
@@ -5,11 +5,13 @@ import payload from 'payload';
 import { CREDENTIAL_BATCH_STATUS } from '../constants/batches';
 import CredentialBatchStatusCell from '../components/batch/CredentialBatchStatusCell';
 import { duplicateBatch } from '../endpoints/duplicateBatch';
+import { tenantAccessFilterQuery, setTenantOnCreate } from '../helpers/tenant';
 
 const CredentialsBatchesCollection: CollectionConfig = {
     slug: 'credential-batch',
     labels: { plural: 'Issuance Overview' },
     access: {
+        read: tenantAccessFilterQuery,
         delete: async ({ id }) => {
             try {
                 if (!id) return true;
@@ -63,7 +65,9 @@ const CredentialsBatchesCollection: CollectionConfig = {
         },
     },
     endpoints: [{ path: '/:id/duplicate', method: 'post', handler: duplicateBatch }],
+    hooks: { beforeChange: [setTenantOnCreate] },
     fields: [
+        { name: 'tenant', type: 'text', hidden: true },
         {
             name: 'title',
             label: 'Batch Title',
diff --git a/src/collections/CredentialTemplates.ts b/src/collections/CredentialTemplates.ts
index b93e7d3..a140bdc 100644
--- a/src/collections/CredentialTemplates.ts
+++ b/src/collections/CredentialTemplates.ts
@@ -2,6 +2,7 @@ import { CollectionConfig } from 'payload/types';
 import CreateTemplate from '../components/template/CreateTemplate';
 import TemplatePageDescription from '../components/template/TemplatePageDescription';
 import CodeEditorWithCsvValidation from '../components/template/CodeEditorWithCsvValidation';
+import { tenantAccessFilterQuery, setTenantOnCreate } from '../helpers/tenant';
 
 const CredentialsTemplatesCollection: CollectionConfig = {
     slug: 'credential-template',
@@ -15,7 +16,10 @@ const CredentialsTemplatesCollection: CollectionConfig = {
             },
         },
     },
+    access: { read: tenantAccessFilterQuery },
+    hooks: { beforeChange: [setTenantOnCreate] },
     fields: [
+        { name: 'tenant', type: 'text', hidden: true },
         {
             name: 'title',
             type: 'text',
diff --git a/src/collections/Credentials.ts b/src/collections/Credentials.ts
index 59995a1..7859351 100644
--- a/src/collections/Credentials.ts
+++ b/src/collections/Credentials.ts
@@ -6,6 +6,7 @@ import CredentialStatusCell from '../components/credential/CredentialStatusCell'
 import { CREDENTIAL_STATUS } from '../constants/credentials';
 import DefaultListView from '../components/List/DefaultListView';
 import CreateCredential from '../components/credential/CreateCredential';
+import { tenantAccessFilterQuery, setTenantOnCreate } from '../helpers/tenant';
 
 const CredentialsCollection: CollectionConfig = {
     slug: 'credential',
@@ -17,6 +18,7 @@ const CredentialsCollection: CollectionConfig = {
         components: { views: { List: DefaultListView, Edit: CreateCredential } },
     },
     access: {
+        read: tenantAccessFilterQuery,
         create: () => false,
         update: async ({ id }) => {
             try {
@@ -55,7 +57,9 @@ const CredentialsCollection: CollectionConfig = {
             }
         },
     },
+    hooks: { beforeChange: [setTenantOnCreate] },
     fields: [
+        { name: 'tenant', type: 'text', hidden: true },
         { name: 'credentialName', type: 'text' },
         { name: 'earnerName', type: 'text' },
         { name: 'emailAddress', type: 'email' },
diff --git a/src/collections/EmailTemplates.ts b/src/collections/EmailTemplates.ts
index 29c9d50..0d5d6e7 100644
--- a/src/collections/EmailTemplates.ts
+++ b/src/collections/EmailTemplates.ts
@@ -2,6 +2,7 @@ import { CollectionConfig } from 'payload/types';
 import CreateEmailTemplate from '../components/email-template/CreateEmailTemplate';
 import CodeEditorWithCsvValidation from '../components/email-template/CodeEditorWithCsvValidation';
 import EmailPageDescription from '../components/Email/EmailPageDescription';
+import { tenantAccessFilterQuery, setTenantOnCreate } from '../helpers/tenant';
 
 const placeholderEmailData = `
   <html>
@@ -28,7 +29,10 @@ const EmailTemplatesCollection: CollectionConfig = {
             },
         },
     },
+    access: { read: tenantAccessFilterQuery },
+    hooks: { beforeChange: [setTenantOnCreate] },
     fields: [
+        { name: 'tenant', type: 'text', hidden: true },
         {
             name: 'title',
             type: 'text',
diff --git a/src/collections/Examples.ts b/src/collections/Examples.ts
deleted file mode 100644
index bb3dab1..0000000
--- a/src/collections/Examples.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { CollectionConfig } from 'payload/types';
-
-// Example Collection - For reference only, this must be added to payload.config.ts to be used.
-const Examples: CollectionConfig = {
-  slug: 'examples',
-  admin: {
-    useAsTitle: 'someField',
-  },
-  fields: [
-    {
-      name: 'someField',
-      type: 'text',
-    },
-  ],
-}
-
-export default Examples;
\ No newline at end of file
diff --git a/src/collections/Users.ts b/src/collections/Users.ts
index dd09947..557dbc7 100644
--- a/src/collections/Users.ts
+++ b/src/collections/Users.ts
@@ -1,17 +1,57 @@
 import { CollectionConfig } from 'payload/types';
 import UserPageDescription from '../components/User/UserPageDescription';
 import CreateUser from '../components/User/CreateUser';
+import { tenantAccessFilterQuery, setTenantOnCreate } from '../helpers/tenant';
+
+const isAdmin = ({ req }) => req.user.isAdmin;
 
 const Users: CollectionConfig = {
-    slug: 'users',
-    auth: { tokenExpiration: 7 * 24 * 60 * 60 },
-    admin: {
-        useAsTitle: 'name',
-        defaultColumns: ['name', 'email'],
-        description: UserPageDescription,
-        components: { views: { Edit: CreateUser } },
+  slug: 'users',
+  auth: { tokenExpiration: 7 * 24 * 60 * 60 },
+  admin: {
+    useAsTitle: 'name',
+    defaultColumns: ['name', 'email'],
+    description: UserPageDescription,
+    components: { views: { Edit: CreateUser } },
+  },
+  access: {
+    read: ({ req }) => ({ tenant: { equals: req.headers.host } }),
+    create: (args) => isAdmin(args) ?
+      tenantAccessFilterQuery(args) : false,
+    update: (args) => isAdmin(args) ?
+      tenantAccessFilterQuery(args) : false,
+    delete: (args) => isAdmin(args) ?
+      tenantAccessFilterQuery(args) : false,
+  },
+  hooks: { beforeChange: [setTenantOnCreate] },
+  fields: [
+    { name: 'name', type: 'text' },
+    {
+      name: 'isAdmin',
+      label: 'Is admin for all tenants',
+      type: 'checkbox',
+      defaultValue: false,
+      saveToJWT: true,
+      access: {
+        create: isAdmin,
+        update: isAdmin,
+        read: isAdmin,
+      },
+    },
+    {
+      name: 'tenant',
+      hidden: true,
+      type: 'text',
+      saveToJWT: true,
+      access: {
+        create: isAdmin,
+        update: isAdmin,
+        read: isAdmin,
+      },
     },
-    fields: [{ name: 'name', type: 'text' }],
+
+  ],
 };
 
+
 export default Users;
diff --git a/src/components/SideNav/SideNav.tsx b/src/components/SideNav/SideNav.tsx
index f5d8982..64bd899 100644
--- a/src/components/SideNav/SideNav.tsx
+++ b/src/components/SideNav/SideNav.tsx
@@ -110,7 +110,6 @@ const SideNav: React.FC = () => {
                     <img src={MailPlus} alt="email-template" />
                     <span className={`transition-[font-size]`}>Email Templates</span>
                 </NavLink>
-
                 <NavLink className="navbar-buttons" to="/admin/collections/users" onClick={close}>
                     <img src={Users} alt="users" />
                     <span className={`transition-[font-size]`}>Users</span>
diff --git a/src/components/User/UserPageDescription.tsx b/src/components/User/UserPageDescription.tsx
index ec75521..1c436ef 100644
--- a/src/components/User/UserPageDescription.tsx
+++ b/src/components/User/UserPageDescription.tsx
@@ -2,13 +2,16 @@ import React from 'react';
 import { useConfig } from 'payload/dist/admin/components/utilities/Config';
 import { Link } from 'react-router-dom';
 import './UserPageDescription.scss';
+import { useAuth } from 'payload/dist/admin/components/utilities/Auth';
 
 const UserPageDescription: React.FC = () => {
     const {
         routes: { admin: adminRoute },
     } = useConfig();
 
-    return (
+    const { user } = useAuth()
+
+    return user.isAdmin ? (
         <div className="header_wrapper">
             <p className="header_paragraph"></p>
             <Link className="header_template_button" to={`${adminRoute}/collections/users/create`}>
@@ -16,7 +19,7 @@ const UserPageDescription: React.FC = () => {
                 Create New User
             </Link>
         </div>
-    );
+    ) : null;
 };
 
 export default UserPageDescription;
diff --git a/src/endpoints/getCollectionCount.ts b/src/endpoints/getCollectionCount.ts
index 1d43688..d3542aa 100644
--- a/src/endpoints/getCollectionCount.ts
+++ b/src/endpoints/getCollectionCount.ts
@@ -6,6 +6,7 @@ export const getCollectionCount: PayloadHandler = async (req, res) => {
     if (!req.user) return res.sendStatus(401);
     const collectionName = req?.body?.collectionName;
     const payloadQuery = req?.body?.query;
+    payloadQuery["tenant"] = req.headers.host;
 
     try {
         const data = await payload.collections[collectionName].Model.count({ ...payloadQuery });
diff --git a/src/endpoints/getCredentialLinks.ts b/src/endpoints/getCredentialLinks.ts
index 33e1d5a..95269fb 100644
--- a/src/endpoints/getCredentialLinks.ts
+++ b/src/endpoints/getCredentialLinks.ts
@@ -8,7 +8,6 @@ const coordinatorUrl = process.env.COORDINATOR_URL ?? 'http://localhost:4005';
 const secret =
     process.env.PAYLOAD_SECRET ??
     'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaa';
-const tenantName = process.env.TENANT_NAME ?? 'test';
 
 export const getCredentialLinks: PayloadHandler = async (req, res) => {
     let id: string;
@@ -58,7 +57,7 @@ export const getCredentialLinks: PayloadHandler = async (req, res) => {
             headers: { 'Content-Type': 'application/json' },
             body: JSON.stringify({
                 data: [{ vc: builtCredential, retrievalId: id }],
-                tenantName,
+                tenantName: credential.tenant,
             }),
         });
 
diff --git a/src/generated-schema.graphql b/src/generated-schema.graphql
index 3922ea9..2b6dd4f 100644
--- a/src/generated-schema.graphql
+++ b/src/generated-schema.graphql
@@ -7,8 +7,6 @@ type Query {
   CredentialTemplate(id: String!, draft: Boolean): CredentialTemplate
   CredentialTemplates(where: CredentialTemplate_where, draft: Boolean, page: Int, limit: Int, sort: String): CredentialTemplates
   docAccessCredentialTemplate(id: String!): credential_templateDocAccess
-  versionCredentialTemplate(id: String): CredentialTemplateVersion
-  versionsCredentialTemplates(where: versionsCredentialTemplate_where, page: Int, limit: Int, sort: String): versionsCredentialTemplates
   CredentialBatch(id: String!, draft: Boolean): CredentialBatch
   CredentialBatches(where: CredentialBatch_where, draft: Boolean, page: Int, limit: Int, sort: String): CredentialBatches
   docAccessCredentialBatch(id: String!): credential_batchDocAccess
@@ -17,13 +15,9 @@ type Query {
   Credential(id: String!, draft: Boolean): Credential
   Credentials(where: Credential_where, draft: Boolean, page: Int, limit: Int, sort: String): Credentials
   docAccessCredential(id: String!): credentialDocAccess
-  versionCredential(id: String): CredentialVersion
-  versionsCredentials(where: versionsCredential_where, page: Int, limit: Int, sort: String): versionsCredentials
   EmailTemplate(id: String!, draft: Boolean): EmailTemplate
   EmailTemplates(where: EmailTemplate_where, draft: Boolean, page: Int, limit: Int, sort: String): EmailTemplates
   docAccessEmailTemplate(id: String!): email_templateDocAccess
-  versionEmailTemplate(id: String): EmailTemplateVersion
-  versionsEmailTemplates(where: versionsEmailTemplate_where, page: Int, limit: Int, sort: String): versionsEmailTemplates
   Preference(key: String): Preference
   Access: Access
 }
@@ -31,6 +25,8 @@ type Query {
 type User {
   id: String
   name: String
+  isAdmin: Boolean
+  tenant: String
   updatedAt: DateTime
   createdAt: DateTime
   email: EmailAddress!
@@ -69,6 +65,8 @@ type Users {
 
 input User_where {
   name: User_name_operator
+  isAdmin: User_isAdmin_operator
+  tenant: User_tenant_operator
   updatedAt: User_updatedAt_operator
   createdAt: User_createdAt_operator
   email: User_email_operator
@@ -88,6 +86,23 @@ input User_name_operator {
   exists: Boolean
 }
 
+input User_isAdmin_operator {
+  equals: Boolean
+  not_equals: Boolean
+  exists: Boolean
+}
+
+input User_tenant_operator {
+  equals: String
+  not_equals: String
+  like: String
+  contains: String
+  in: [String]
+  not_in: [String]
+  all: [String]
+  exists: Boolean
+}
+
 input User_updatedAt_operator {
   equals: DateTime
   not_equals: DateTime
@@ -133,6 +148,8 @@ input User_id_operator {
 
 input User_where_or {
   name: User_name_operator
+  isAdmin: User_isAdmin_operator
+  tenant: User_tenant_operator
   updatedAt: User_updatedAt_operator
   createdAt: User_createdAt_operator
   email: User_email_operator
@@ -141,6 +158,8 @@ input User_where_or {
 
 input User_where_and {
   name: User_name_operator
+  isAdmin: User_isAdmin_operator
+  tenant: User_tenant_operator
   updatedAt: User_updatedAt_operator
   createdAt: User_createdAt_operator
   email: User_email_operator
@@ -158,6 +177,8 @@ type usersDocAccess {
 
 type UsersDocAccessFields {
   name: UsersDocAccessFields_name
+  isAdmin: UsersDocAccessFields_isAdmin
+  tenant: UsersDocAccessFields_tenant
   updatedAt: UsersDocAccessFields_updatedAt
   createdAt: UsersDocAccessFields_createdAt
   email: UsersDocAccessFields_email
@@ -187,6 +208,52 @@ type UsersDocAccessFields_name_Delete {
   permission: Boolean!
 }
 
+type UsersDocAccessFields_isAdmin {
+  create: UsersDocAccessFields_isAdmin_Create
+  read: UsersDocAccessFields_isAdmin_Read
+  update: UsersDocAccessFields_isAdmin_Update
+  delete: UsersDocAccessFields_isAdmin_Delete
+}
+
+type UsersDocAccessFields_isAdmin_Create {
+  permission: Boolean!
+}
+
+type UsersDocAccessFields_isAdmin_Read {
+  permission: Boolean!
+}
+
+type UsersDocAccessFields_isAdmin_Update {
+  permission: Boolean!
+}
+
+type UsersDocAccessFields_isAdmin_Delete {
+  permission: Boolean!
+}
+
+type UsersDocAccessFields_tenant {
+  create: UsersDocAccessFields_tenant_Create
+  read: UsersDocAccessFields_tenant_Read
+  update: UsersDocAccessFields_tenant_Update
+  delete: UsersDocAccessFields_tenant_Delete
+}
+
+type UsersDocAccessFields_tenant_Create {
+  permission: Boolean!
+}
+
+type UsersDocAccessFields_tenant_Read {
+  permission: Boolean!
+}
+
+type UsersDocAccessFields_tenant_Update {
+  permission: Boolean!
+}
+
+type UsersDocAccessFields_tenant_Delete {
+  permission: Boolean!
+}
+
 type UsersDocAccessFields_updatedAt {
   create: UsersDocAccessFields_updatedAt_Create
   read: UsersDocAccessFields_updatedAt_Read
@@ -318,13 +385,13 @@ type usersMe {
 
 type CredentialTemplate {
   id: String
-  title: String
+  tenant: String
+  title: String!
   description: String
   internalNotes: String
-  credentialTemplateJson: JSON
+  credentialTemplateJson: JSON!
   updatedAt: DateTime
   createdAt: DateTime
-  _status: CredentialTemplate__status
 }
 
 """
@@ -332,11 +399,6 @@ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://
 """
 scalar JSON
 
-enum CredentialTemplate__status {
-  draft
-  published
-}
-
 type CredentialTemplates {
   docs: [CredentialTemplate]
   totalDocs: Int
@@ -358,7 +420,6 @@ input CredentialTemplate_where {
   credentialTemplateJson: CredentialTemplate_credentialTemplateJson_operator
   updatedAt: CredentialTemplate_updatedAt_operator
   createdAt: CredentialTemplate_createdAt_operator
-  _status: CredentialTemplate__status_operator
   id: CredentialTemplate_id_operator
   OR: [CredentialTemplate_where_or]
   AND: [CredentialTemplate_where_and]
@@ -419,20 +480,6 @@ input CredentialTemplate_createdAt_operator {
   exists: Boolean
 }
 
-input CredentialTemplate__status_operator {
-  equals: CredentialTemplate__status_Input
-  not_equals: CredentialTemplate__status_Input
-  in: [CredentialTemplate__status_Input]
-  not_in: [CredentialTemplate__status_Input]
-  all: [CredentialTemplate__status_Input]
-  exists: Boolean
-}
-
-enum CredentialTemplate__status_Input {
-  draft
-  published
-}
-
 input CredentialTemplate_id_operator {
   equals: String
   not_equals: String
@@ -451,7 +498,6 @@ input CredentialTemplate_where_or {
   credentialTemplateJson: CredentialTemplate_credentialTemplateJson_operator
   updatedAt: CredentialTemplate_updatedAt_operator
   createdAt: CredentialTemplate_createdAt_operator
-  _status: CredentialTemplate__status_operator
   id: CredentialTemplate_id_operator
 }
 
@@ -462,7 +508,6 @@ input CredentialTemplate_where_and {
   credentialTemplateJson: CredentialTemplate_credentialTemplateJson_operator
   updatedAt: CredentialTemplate_updatedAt_operator
   createdAt: CredentialTemplate_createdAt_operator
-  _status: CredentialTemplate__status_operator
   id: CredentialTemplate_id_operator
 }
 
@@ -472,7 +517,6 @@ type credential_templateDocAccess {
   read: CredentialTemplateReadDocAccess
   update: CredentialTemplateUpdateDocAccess
   delete: CredentialTemplateDeleteDocAccess
-  readVersions: CredentialTemplateReadVersionsDocAccess
 }
 
 type CredentialTemplateDocAccessFields {
@@ -482,7 +526,6 @@ type CredentialTemplateDocAccessFields {
   credentialTemplateJson: CredentialTemplateDocAccessFields_credentialTemplateJson
   updatedAt: CredentialTemplateDocAccessFields_updatedAt
   createdAt: CredentialTemplateDocAccessFields_createdAt
-  _status: CredentialTemplateDocAccessFields__status
 }
 
 type CredentialTemplateDocAccessFields_title {
@@ -623,29 +666,6 @@ type CredentialTemplateDocAccessFields_createdAt_Delete {
   permission: Boolean!
 }
 
-type CredentialTemplateDocAccessFields__status {
-  create: CredentialTemplateDocAccessFields__status_Create
-  read: CredentialTemplateDocAccessFields__status_Read
-  update: CredentialTemplateDocAccessFields__status_Update
-  delete: CredentialTemplateDocAccessFields__status_Delete
-}
-
-type CredentialTemplateDocAccessFields__status_Create {
-  permission: Boolean!
-}
-
-type CredentialTemplateDocAccessFields__status_Read {
-  permission: Boolean!
-}
-
-type CredentialTemplateDocAccessFields__status_Update {
-  permission: Boolean!
-}
-
-type CredentialTemplateDocAccessFields__status_Delete {
-  permission: Boolean!
-}
-
 type CredentialTemplateCreateDocAccess {
   permission: Boolean!
   where: JSONObject
@@ -666,211 +686,16 @@ type CredentialTemplateDeleteDocAccess {
   where: JSONObject
 }
 
-type CredentialTemplateReadVersionsDocAccess {
-  permission: Boolean!
-  where: JSONObject
-}
-
-type CredentialTemplateVersion {
-  parent: CredentialTemplate
-  version: CredentialTemplateVersion_Version
-  createdAt: DateTime
-  updatedAt: DateTime
-  id: String
-}
-
-type CredentialTemplateVersion_Version {
-  title: String
-  description: String
-  internalNotes: String
-  credentialTemplateJson: JSON
-  updatedAt: DateTime
-  createdAt: DateTime
-  _status: CredentialTemplateVersion_Version__status
-}
-
-enum CredentialTemplateVersion_Version__status {
-  draft
-  published
-}
-
-type versionsCredentialTemplates {
-  docs: [CredentialTemplateVersion]
-  totalDocs: Int
-  offset: Int
-  limit: Int
-  totalPages: Int
-  page: Int
-  pagingCounter: Int
-  hasPrevPage: Boolean
-  hasNextPage: Boolean
-  prevPage: Int
-  nextPage: Int
-}
-
-input versionsCredentialTemplate_where {
-  parent: versionsCredentialTemplate_parent_operator
-  version__title: versionsCredentialTemplate_version__title_operator
-  version__description: versionsCredentialTemplate_version__description_operator
-  version__internalNotes: versionsCredentialTemplate_version__internalNotes_operator
-  version__credentialTemplateJson: versionsCredentialTemplate_version__credentialTemplateJson_operator
-  version__updatedAt: versionsCredentialTemplate_version__updatedAt_operator
-  version__createdAt: versionsCredentialTemplate_version__createdAt_operator
-  version___status: versionsCredentialTemplate_version___status_operator
-  createdAt: versionsCredentialTemplate_createdAt_operator
-  updatedAt: versionsCredentialTemplate_updatedAt_operator
-  id: versionsCredentialTemplate_id_operator
-  OR: [versionsCredentialTemplate_where_or]
-  AND: [versionsCredentialTemplate_where_and]
-}
-
-input versionsCredentialTemplate_parent_operator {
-  equals: String
-  not_equals: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-  exists: Boolean
-}
-
-input versionsCredentialTemplate_version__title_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-}
-
-input versionsCredentialTemplate_version__description_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  exists: Boolean
-}
-
-input versionsCredentialTemplate_version__internalNotes_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  exists: Boolean
-}
-
-input versionsCredentialTemplate_version__credentialTemplateJson_operator {
-  equals: JSON
-  not_equals: JSON
-  like: JSON
-  contains: JSON
-}
-
-input versionsCredentialTemplate_version__updatedAt_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
-}
-
-input versionsCredentialTemplate_version__createdAt_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
-}
-
-input versionsCredentialTemplate_version___status_operator {
-  equals: versionsCredentialTemplate_version___status_Input
-  not_equals: versionsCredentialTemplate_version___status_Input
-  in: [versionsCredentialTemplate_version___status_Input]
-  not_in: [versionsCredentialTemplate_version___status_Input]
-  all: [versionsCredentialTemplate_version___status_Input]
-  exists: Boolean
-}
-
-enum versionsCredentialTemplate_version___status_Input {
-  draft
-  published
-}
-
-input versionsCredentialTemplate_createdAt_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
-}
-
-input versionsCredentialTemplate_updatedAt_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
-}
-
-input versionsCredentialTemplate_id_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-  exists: Boolean
-}
-
-input versionsCredentialTemplate_where_or {
-  parent: versionsCredentialTemplate_parent_operator
-  version__title: versionsCredentialTemplate_version__title_operator
-  version__description: versionsCredentialTemplate_version__description_operator
-  version__internalNotes: versionsCredentialTemplate_version__internalNotes_operator
-  version__credentialTemplateJson: versionsCredentialTemplate_version__credentialTemplateJson_operator
-  version__updatedAt: versionsCredentialTemplate_version__updatedAt_operator
-  version__createdAt: versionsCredentialTemplate_version__createdAt_operator
-  version___status: versionsCredentialTemplate_version___status_operator
-  createdAt: versionsCredentialTemplate_createdAt_operator
-  updatedAt: versionsCredentialTemplate_updatedAt_operator
-  id: versionsCredentialTemplate_id_operator
-}
-
-input versionsCredentialTemplate_where_and {
-  parent: versionsCredentialTemplate_parent_operator
-  version__title: versionsCredentialTemplate_version__title_operator
-  version__description: versionsCredentialTemplate_version__description_operator
-  version__internalNotes: versionsCredentialTemplate_version__internalNotes_operator
-  version__credentialTemplateJson: versionsCredentialTemplate_version__credentialTemplateJson_operator
-  version__updatedAt: versionsCredentialTemplate_version__updatedAt_operator
-  version__createdAt: versionsCredentialTemplate_version__createdAt_operator
-  version___status: versionsCredentialTemplate_version___status_operator
-  createdAt: versionsCredentialTemplate_createdAt_operator
-  updatedAt: versionsCredentialTemplate_updatedAt_operator
-  id: versionsCredentialTemplate_id_operator
-}
-
 type CredentialBatch {
   id: String
+  tenant: String
   title: String
   description: String
   internalNotes: String
   status: String
   template: CredentialTemplate
   emailTemplate: EmailTemplate
+  from: String
   csvFields: JSON
   updatedAt: DateTime
   createdAt: DateTime
@@ -879,18 +704,14 @@ type CredentialBatch {
 
 type EmailTemplate {
   id: String
-  title: String
+  tenant: String
+  title: String!
   internalNotes: String
+  from: String
   emailSubjectTitle: String
-  emailTemplatesHandlebarsCode: String
+  emailTemplatesHandlebarsCode: String!
   updatedAt: DateTime
   createdAt: DateTime
-  _status: EmailTemplate__status
-}
-
-enum EmailTemplate__status {
-  draft
-  published
 }
 
 enum CredentialBatch__status {
@@ -919,6 +740,7 @@ input CredentialBatch_where {
   status: CredentialBatch_status_operator
   template: CredentialBatch_template_operator
   emailTemplate: CredentialBatch_emailTemplate_operator
+  from: CredentialBatch_from_operator
   csvFields: CredentialBatch_csvFields_operator
   updatedAt: CredentialBatch_updatedAt_operator
   createdAt: CredentialBatch_createdAt_operator
@@ -980,6 +802,17 @@ input CredentialBatch_emailTemplate_operator {
   all: [String]
 }
 
+input CredentialBatch_from_operator {
+  equals: String
+  not_equals: String
+  like: String
+  contains: String
+  in: [String]
+  not_in: [String]
+  all: [String]
+  exists: Boolean
+}
+
 input CredentialBatch_csvFields_operator {
   equals: JSON
   not_equals: JSON
@@ -1042,6 +875,7 @@ input CredentialBatch_where_or {
   status: CredentialBatch_status_operator
   template: CredentialBatch_template_operator
   emailTemplate: CredentialBatch_emailTemplate_operator
+  from: CredentialBatch_from_operator
   csvFields: CredentialBatch_csvFields_operator
   updatedAt: CredentialBatch_updatedAt_operator
   createdAt: CredentialBatch_createdAt_operator
@@ -1056,6 +890,7 @@ input CredentialBatch_where_and {
   status: CredentialBatch_status_operator
   template: CredentialBatch_template_operator
   emailTemplate: CredentialBatch_emailTemplate_operator
+  from: CredentialBatch_from_operator
   csvFields: CredentialBatch_csvFields_operator
   updatedAt: CredentialBatch_updatedAt_operator
   createdAt: CredentialBatch_createdAt_operator
@@ -1079,6 +914,7 @@ type CredentialBatchDocAccessFields {
   status: CredentialBatchDocAccessFields_status
   template: CredentialBatchDocAccessFields_template
   emailTemplate: CredentialBatchDocAccessFields_emailTemplate
+  from: CredentialBatchDocAccessFields_from
   csvFields: CredentialBatchDocAccessFields_csvFields
   updatedAt: CredentialBatchDocAccessFields_updatedAt
   createdAt: CredentialBatchDocAccessFields_createdAt
@@ -1223,6 +1059,29 @@ type CredentialBatchDocAccessFields_emailTemplate_Delete {
   permission: Boolean!
 }
 
+type CredentialBatchDocAccessFields_from {
+  create: CredentialBatchDocAccessFields_from_Create
+  read: CredentialBatchDocAccessFields_from_Read
+  update: CredentialBatchDocAccessFields_from_Update
+  delete: CredentialBatchDocAccessFields_from_Delete
+}
+
+type CredentialBatchDocAccessFields_from_Create {
+  permission: Boolean!
+}
+
+type CredentialBatchDocAccessFields_from_Read {
+  permission: Boolean!
+}
+
+type CredentialBatchDocAccessFields_from_Update {
+  permission: Boolean!
+}
+
+type CredentialBatchDocAccessFields_from_Delete {
+  permission: Boolean!
+}
+
 type CredentialBatchDocAccessFields_csvFields {
   create: CredentialBatchDocAccessFields_csvFields_Create
   read: CredentialBatchDocAccessFields_csvFields_Read
@@ -1350,12 +1209,14 @@ type CredentialBatchVersion {
 }
 
 type CredentialBatchVersion_Version {
+  tenant: String
   title: String
   description: String
   internalNotes: String
   status: String
   template: CredentialTemplate
   emailTemplate: EmailTemplate
+  from: String
   csvFields: JSON
   updatedAt: DateTime
   createdAt: DateTime
@@ -1383,12 +1244,14 @@ type versionsCredentialBatches {
 
 input versionsCredentialBatch_where {
   parent: versionsCredentialBatch_parent_operator
+  version__tenant: versionsCredentialBatch_version__tenant_operator
   version__title: versionsCredentialBatch_version__title_operator
   version__description: versionsCredentialBatch_version__description_operator
   version__internalNotes: versionsCredentialBatch_version__internalNotes_operator
   version__status: versionsCredentialBatch_version__status_operator
   version__template: versionsCredentialBatch_version__template_operator
   version__emailTemplate: versionsCredentialBatch_version__emailTemplate_operator
+  version__from: versionsCredentialBatch_version__from_operator
   version__csvFields: versionsCredentialBatch_version__csvFields_operator
   version__updatedAt: versionsCredentialBatch_version__updatedAt_operator
   version__createdAt: versionsCredentialBatch_version__createdAt_operator
@@ -1410,6 +1273,17 @@ input versionsCredentialBatch_parent_operator {
   exists: Boolean
 }
 
+input versionsCredentialBatch_version__tenant_operator {
+  equals: String
+  not_equals: String
+  like: String
+  contains: String
+  in: [String]
+  not_in: [String]
+  all: [String]
+  exists: Boolean
+}
+
 input versionsCredentialBatch_version__title_operator {
   equals: String
   not_equals: String
@@ -1462,6 +1336,17 @@ input versionsCredentialBatch_version__emailTemplate_operator {
   all: [String]
 }
 
+input versionsCredentialBatch_version__from_operator {
+  equals: String
+  not_equals: String
+  like: String
+  contains: String
+  in: [String]
+  not_in: [String]
+  all: [String]
+  exists: Boolean
+}
+
 input versionsCredentialBatch_version__csvFields_operator {
   equals: JSON
   not_equals: JSON
@@ -1547,12 +1432,14 @@ input versionsCredentialBatch_id_operator {
 
 input versionsCredentialBatch_where_or {
   parent: versionsCredentialBatch_parent_operator
+  version__tenant: versionsCredentialBatch_version__tenant_operator
   version__title: versionsCredentialBatch_version__title_operator
   version__description: versionsCredentialBatch_version__description_operator
   version__internalNotes: versionsCredentialBatch_version__internalNotes_operator
   version__status: versionsCredentialBatch_version__status_operator
   version__template: versionsCredentialBatch_version__template_operator
   version__emailTemplate: versionsCredentialBatch_version__emailTemplate_operator
+  version__from: versionsCredentialBatch_version__from_operator
   version__csvFields: versionsCredentialBatch_version__csvFields_operator
   version__updatedAt: versionsCredentialBatch_version__updatedAt_operator
   version__createdAt: versionsCredentialBatch_version__createdAt_operator
@@ -1565,12 +1452,14 @@ input versionsCredentialBatch_where_or {
 
 input versionsCredentialBatch_where_and {
   parent: versionsCredentialBatch_parent_operator
+  version__tenant: versionsCredentialBatch_version__tenant_operator
   version__title: versionsCredentialBatch_version__title_operator
   version__description: versionsCredentialBatch_version__description_operator
   version__internalNotes: versionsCredentialBatch_version__internalNotes_operator
   version__status: versionsCredentialBatch_version__status_operator
   version__template: versionsCredentialBatch_version__template_operator
   version__emailTemplate: versionsCredentialBatch_version__emailTemplate_operator
+  version__from: versionsCredentialBatch_version__from_operator
   version__csvFields: versionsCredentialBatch_version__csvFields_operator
   version__updatedAt: versionsCredentialBatch_version__updatedAt_operator
   version__createdAt: versionsCredentialBatch_version__createdAt_operator
@@ -1583,23 +1472,18 @@ input versionsCredentialBatch_where_and {
 
 type Credential {
   id: String
+  tenant: String
   credentialName: String
   earnerName: String
   emailAddress: EmailAddress
   extraFields: JSON
-  status: String
-  batch: CredentialBatch
+  status: String!
+  batch: CredentialBatch!
   revocationReason: String
   revocationDate: DateTime
   revokedBy: User
   updatedAt: DateTime
   createdAt: DateTime
-  _status: Credential__status
-}
-
-enum Credential__status {
-  draft
-  published
 }
 
 type Credentials {
@@ -1628,7 +1512,6 @@ input Credential_where {
   revokedBy: Credential_revokedBy_operator
   updatedAt: Credential_updatedAt_operator
   createdAt: Credential_createdAt_operator
-  _status: Credential__status_operator
   id: Credential_id_operator
   OR: [Credential_where_or]
   AND: [Credential_where_and]
@@ -1746,20 +1629,6 @@ input Credential_createdAt_operator {
   exists: Boolean
 }
 
-input Credential__status_operator {
-  equals: Credential__status_Input
-  not_equals: Credential__status_Input
-  in: [Credential__status_Input]
-  not_in: [Credential__status_Input]
-  all: [Credential__status_Input]
-  exists: Boolean
-}
-
-enum Credential__status_Input {
-  draft
-  published
-}
-
 input Credential_id_operator {
   equals: String
   not_equals: String
@@ -1783,7 +1652,6 @@ input Credential_where_or {
   revokedBy: Credential_revokedBy_operator
   updatedAt: Credential_updatedAt_operator
   createdAt: Credential_createdAt_operator
-  _status: Credential__status_operator
   id: Credential_id_operator
 }
 
@@ -1799,7 +1667,6 @@ input Credential_where_and {
   revokedBy: Credential_revokedBy_operator
   updatedAt: Credential_updatedAt_operator
   createdAt: Credential_createdAt_operator
-  _status: Credential__status_operator
   id: Credential_id_operator
 }
 
@@ -1809,7 +1676,6 @@ type credentialDocAccess {
   read: CredentialReadDocAccess
   update: CredentialUpdateDocAccess
   delete: CredentialDeleteDocAccess
-  readVersions: CredentialReadVersionsDocAccess
 }
 
 type CredentialDocAccessFields {
@@ -1825,7 +1691,6 @@ type CredentialDocAccessFields {
   actionButton: CredentialDocAccessFields_actionButton
   updatedAt: CredentialDocAccessFields_updatedAt
   createdAt: CredentialDocAccessFields_createdAt
-  _status: CredentialDocAccessFields__status
 }
 
 type CredentialDocAccessFields_credentialName {
@@ -2104,29 +1969,6 @@ type CredentialDocAccessFields_createdAt_Delete {
   permission: Boolean!
 }
 
-type CredentialDocAccessFields__status {
-  create: CredentialDocAccessFields__status_Create
-  read: CredentialDocAccessFields__status_Read
-  update: CredentialDocAccessFields__status_Update
-  delete: CredentialDocAccessFields__status_Delete
-}
-
-type CredentialDocAccessFields__status_Create {
-  permission: Boolean!
-}
-
-type CredentialDocAccessFields__status_Read {
-  permission: Boolean!
-}
-
-type CredentialDocAccessFields__status_Update {
-  permission: Boolean!
-}
-
-type CredentialDocAccessFields__status_Delete {
-  permission: Boolean!
-}
-
 type CredentialCreateDocAccess {
   permission: Boolean!
   where: JSONObject
@@ -2147,280 +1989,6 @@ type CredentialDeleteDocAccess {
   where: JSONObject
 }
 
-type CredentialReadVersionsDocAccess {
-  permission: Boolean!
-  where: JSONObject
-}
-
-type CredentialVersion {
-  parent: Credential
-  version: CredentialVersion_Version
-  createdAt: DateTime
-  updatedAt: DateTime
-  id: String
-}
-
-type CredentialVersion_Version {
-  credentialName: String
-  earnerName: String
-  emailAddress: EmailAddress
-  extraFields: JSON
-  status: String
-  batch: CredentialBatch
-  revocationReason: String
-  revocationDate: DateTime
-  revokedBy: User
-  updatedAt: DateTime
-  createdAt: DateTime
-  _status: CredentialVersion_Version__status
-}
-
-enum CredentialVersion_Version__status {
-  draft
-  published
-}
-
-type versionsCredentials {
-  docs: [CredentialVersion]
-  totalDocs: Int
-  offset: Int
-  limit: Int
-  totalPages: Int
-  page: Int
-  pagingCounter: Int
-  hasPrevPage: Boolean
-  hasNextPage: Boolean
-  prevPage: Int
-  nextPage: Int
-}
-
-input versionsCredential_where {
-  parent: versionsCredential_parent_operator
-  version__credentialName: versionsCredential_version__credentialName_operator
-  version__earnerName: versionsCredential_version__earnerName_operator
-  version__emailAddress: versionsCredential_version__emailAddress_operator
-  version__extraFields: versionsCredential_version__extraFields_operator
-  version__status: versionsCredential_version__status_operator
-  version__batch: versionsCredential_version__batch_operator
-  version__revocationReason: versionsCredential_version__revocationReason_operator
-  version__revocationDate: versionsCredential_version__revocationDate_operator
-  version__revokedBy: versionsCredential_version__revokedBy_operator
-  version__updatedAt: versionsCredential_version__updatedAt_operator
-  version__createdAt: versionsCredential_version__createdAt_operator
-  version___status: versionsCredential_version___status_operator
-  createdAt: versionsCredential_createdAt_operator
-  updatedAt: versionsCredential_updatedAt_operator
-  id: versionsCredential_id_operator
-  OR: [versionsCredential_where_or]
-  AND: [versionsCredential_where_and]
-}
-
-input versionsCredential_parent_operator {
-  equals: String
-  not_equals: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-  exists: Boolean
-}
-
-input versionsCredential_version__credentialName_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-  exists: Boolean
-}
-
-input versionsCredential_version__earnerName_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-  exists: Boolean
-}
-
-input versionsCredential_version__emailAddress_operator {
-  equals: EmailAddress
-  not_equals: EmailAddress
-  like: EmailAddress
-  contains: EmailAddress
-  in: [EmailAddress]
-  not_in: [EmailAddress]
-  all: [EmailAddress]
-  exists: Boolean
-}
-
-input versionsCredential_version__extraFields_operator {
-  equals: JSON
-  not_equals: JSON
-  like: JSON
-  contains: JSON
-  exists: Boolean
-}
-
-input versionsCredential_version__status_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-}
-
-input versionsCredential_version__batch_operator {
-  equals: String
-  not_equals: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-}
-
-input versionsCredential_version__revocationReason_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-  exists: Boolean
-}
-
-input versionsCredential_version__revocationDate_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
-}
-
-input versionsCredential_version__revokedBy_operator {
-  equals: String
-  not_equals: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-  exists: Boolean
-}
-
-input versionsCredential_version__updatedAt_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
-}
-
-input versionsCredential_version__createdAt_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
-}
-
-input versionsCredential_version___status_operator {
-  equals: versionsCredential_version___status_Input
-  not_equals: versionsCredential_version___status_Input
-  in: [versionsCredential_version___status_Input]
-  not_in: [versionsCredential_version___status_Input]
-  all: [versionsCredential_version___status_Input]
-  exists: Boolean
-}
-
-enum versionsCredential_version___status_Input {
-  draft
-  published
-}
-
-input versionsCredential_createdAt_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
-}
-
-input versionsCredential_updatedAt_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
-}
-
-input versionsCredential_id_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-  exists: Boolean
-}
-
-input versionsCredential_where_or {
-  parent: versionsCredential_parent_operator
-  version__credentialName: versionsCredential_version__credentialName_operator
-  version__earnerName: versionsCredential_version__earnerName_operator
-  version__emailAddress: versionsCredential_version__emailAddress_operator
-  version__extraFields: versionsCredential_version__extraFields_operator
-  version__status: versionsCredential_version__status_operator
-  version__batch: versionsCredential_version__batch_operator
-  version__revocationReason: versionsCredential_version__revocationReason_operator
-  version__revocationDate: versionsCredential_version__revocationDate_operator
-  version__revokedBy: versionsCredential_version__revokedBy_operator
-  version__updatedAt: versionsCredential_version__updatedAt_operator
-  version__createdAt: versionsCredential_version__createdAt_operator
-  version___status: versionsCredential_version___status_operator
-  createdAt: versionsCredential_createdAt_operator
-  updatedAt: versionsCredential_updatedAt_operator
-  id: versionsCredential_id_operator
-}
-
-input versionsCredential_where_and {
-  parent: versionsCredential_parent_operator
-  version__credentialName: versionsCredential_version__credentialName_operator
-  version__earnerName: versionsCredential_version__earnerName_operator
-  version__emailAddress: versionsCredential_version__emailAddress_operator
-  version__extraFields: versionsCredential_version__extraFields_operator
-  version__status: versionsCredential_version__status_operator
-  version__batch: versionsCredential_version__batch_operator
-  version__revocationReason: versionsCredential_version__revocationReason_operator
-  version__revocationDate: versionsCredential_version__revocationDate_operator
-  version__revokedBy: versionsCredential_version__revokedBy_operator
-  version__updatedAt: versionsCredential_version__updatedAt_operator
-  version__createdAt: versionsCredential_version__createdAt_operator
-  version___status: versionsCredential_version___status_operator
-  createdAt: versionsCredential_createdAt_operator
-  updatedAt: versionsCredential_updatedAt_operator
-  id: versionsCredential_id_operator
-}
-
 type EmailTemplates {
   docs: [EmailTemplate]
   totalDocs: Int
@@ -2442,7 +2010,6 @@ input EmailTemplate_where {
   emailTemplatesHandlebarsCode: EmailTemplate_emailTemplatesHandlebarsCode_operator
   updatedAt: EmailTemplate_updatedAt_operator
   createdAt: EmailTemplate_createdAt_operator
-  _status: EmailTemplate__status_operator
   id: EmailTemplate_id_operator
   OR: [EmailTemplate_where_or]
   AND: [EmailTemplate_where_and]
@@ -2506,20 +2073,6 @@ input EmailTemplate_createdAt_operator {
   exists: Boolean
 }
 
-input EmailTemplate__status_operator {
-  equals: EmailTemplate__status_Input
-  not_equals: EmailTemplate__status_Input
-  in: [EmailTemplate__status_Input]
-  not_in: [EmailTemplate__status_Input]
-  all: [EmailTemplate__status_Input]
-  exists: Boolean
-}
-
-enum EmailTemplate__status_Input {
-  draft
-  published
-}
-
 input EmailTemplate_id_operator {
   equals: String
   not_equals: String
@@ -2538,7 +2091,6 @@ input EmailTemplate_where_or {
   emailTemplatesHandlebarsCode: EmailTemplate_emailTemplatesHandlebarsCode_operator
   updatedAt: EmailTemplate_updatedAt_operator
   createdAt: EmailTemplate_createdAt_operator
-  _status: EmailTemplate__status_operator
   id: EmailTemplate_id_operator
 }
 
@@ -2549,7 +2101,6 @@ input EmailTemplate_where_and {
   emailTemplatesHandlebarsCode: EmailTemplate_emailTemplatesHandlebarsCode_operator
   updatedAt: EmailTemplate_updatedAt_operator
   createdAt: EmailTemplate_createdAt_operator
-  _status: EmailTemplate__status_operator
   id: EmailTemplate_id_operator
 }
 
@@ -2559,7 +2110,6 @@ type email_templateDocAccess {
   read: EmailTemplateReadDocAccess
   update: EmailTemplateUpdateDocAccess
   delete: EmailTemplateDeleteDocAccess
-  readVersions: EmailTemplateReadVersionsDocAccess
 }
 
 type EmailTemplateDocAccessFields {
@@ -2569,7 +2119,6 @@ type EmailTemplateDocAccessFields {
   emailTemplatesHandlebarsCode: EmailTemplateDocAccessFields_emailTemplatesHandlebarsCode
   updatedAt: EmailTemplateDocAccessFields_updatedAt
   createdAt: EmailTemplateDocAccessFields_createdAt
-  _status: EmailTemplateDocAccessFields__status
 }
 
 type EmailTemplateDocAccessFields_title {
@@ -2649,308 +2198,85 @@ type EmailTemplateDocAccessFields_emailTemplatesHandlebarsCode {
 }
 
 type EmailTemplateDocAccessFields_emailTemplatesHandlebarsCode_Create {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields_emailTemplatesHandlebarsCode_Read {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields_emailTemplatesHandlebarsCode_Update {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields_emailTemplatesHandlebarsCode_Delete {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields_updatedAt {
-  create: EmailTemplateDocAccessFields_updatedAt_Create
-  read: EmailTemplateDocAccessFields_updatedAt_Read
-  update: EmailTemplateDocAccessFields_updatedAt_Update
-  delete: EmailTemplateDocAccessFields_updatedAt_Delete
-}
-
-type EmailTemplateDocAccessFields_updatedAt_Create {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields_updatedAt_Read {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields_updatedAt_Update {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields_updatedAt_Delete {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields_createdAt {
-  create: EmailTemplateDocAccessFields_createdAt_Create
-  read: EmailTemplateDocAccessFields_createdAt_Read
-  update: EmailTemplateDocAccessFields_createdAt_Update
-  delete: EmailTemplateDocAccessFields_createdAt_Delete
-}
-
-type EmailTemplateDocAccessFields_createdAt_Create {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields_createdAt_Read {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields_createdAt_Update {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields_createdAt_Delete {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields__status {
-  create: EmailTemplateDocAccessFields__status_Create
-  read: EmailTemplateDocAccessFields__status_Read
-  update: EmailTemplateDocAccessFields__status_Update
-  delete: EmailTemplateDocAccessFields__status_Delete
-}
-
-type EmailTemplateDocAccessFields__status_Create {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields__status_Read {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields__status_Update {
-  permission: Boolean!
-}
-
-type EmailTemplateDocAccessFields__status_Delete {
-  permission: Boolean!
-}
-
-type EmailTemplateCreateDocAccess {
-  permission: Boolean!
-  where: JSONObject
-}
-
-type EmailTemplateReadDocAccess {
-  permission: Boolean!
-  where: JSONObject
-}
-
-type EmailTemplateUpdateDocAccess {
-  permission: Boolean!
-  where: JSONObject
-}
-
-type EmailTemplateDeleteDocAccess {
-  permission: Boolean!
-  where: JSONObject
-}
-
-type EmailTemplateReadVersionsDocAccess {
-  permission: Boolean!
-  where: JSONObject
-}
-
-type EmailTemplateVersion {
-  parent: EmailTemplate
-  version: EmailTemplateVersion_Version
-  createdAt: DateTime
-  updatedAt: DateTime
-  id: String
+  permission: Boolean!
 }
 
-type EmailTemplateVersion_Version {
-  title: String
-  internalNotes: String
-  emailSubjectTitle: String
-  emailTemplatesHandlebarsCode: String
-  updatedAt: DateTime
-  createdAt: DateTime
-  _status: EmailTemplateVersion_Version__status
+type EmailTemplateDocAccessFields_emailTemplatesHandlebarsCode_Read {
+  permission: Boolean!
 }
 
-enum EmailTemplateVersion_Version__status {
-  draft
-  published
+type EmailTemplateDocAccessFields_emailTemplatesHandlebarsCode_Update {
+  permission: Boolean!
 }
 
-type versionsEmailTemplates {
-  docs: [EmailTemplateVersion]
-  totalDocs: Int
-  offset: Int
-  limit: Int
-  totalPages: Int
-  page: Int
-  pagingCounter: Int
-  hasPrevPage: Boolean
-  hasNextPage: Boolean
-  prevPage: Int
-  nextPage: Int
+type EmailTemplateDocAccessFields_emailTemplatesHandlebarsCode_Delete {
+  permission: Boolean!
 }
 
-input versionsEmailTemplate_where {
-  parent: versionsEmailTemplate_parent_operator
-  version__title: versionsEmailTemplate_version__title_operator
-  version__internalNotes: versionsEmailTemplate_version__internalNotes_operator
-  version__emailSubjectTitle: versionsEmailTemplate_version__emailSubjectTitle_operator
-  version__emailTemplatesHandlebarsCode: versionsEmailTemplate_version__emailTemplatesHandlebarsCode_operator
-  version__updatedAt: versionsEmailTemplate_version__updatedAt_operator
-  version__createdAt: versionsEmailTemplate_version__createdAt_operator
-  version___status: versionsEmailTemplate_version___status_operator
-  createdAt: versionsEmailTemplate_createdAt_operator
-  updatedAt: versionsEmailTemplate_updatedAt_operator
-  id: versionsEmailTemplate_id_operator
-  OR: [versionsEmailTemplate_where_or]
-  AND: [versionsEmailTemplate_where_and]
+type EmailTemplateDocAccessFields_updatedAt {
+  create: EmailTemplateDocAccessFields_updatedAt_Create
+  read: EmailTemplateDocAccessFields_updatedAt_Read
+  update: EmailTemplateDocAccessFields_updatedAt_Update
+  delete: EmailTemplateDocAccessFields_updatedAt_Delete
 }
 
-input versionsEmailTemplate_parent_operator {
-  equals: String
-  not_equals: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-  exists: Boolean
+type EmailTemplateDocAccessFields_updatedAt_Create {
+  permission: Boolean!
 }
 
-input versionsEmailTemplate_version__title_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  in: [String]
-  not_in: [String]
-  all: [String]
+type EmailTemplateDocAccessFields_updatedAt_Read {
+  permission: Boolean!
 }
 
-input versionsEmailTemplate_version__internalNotes_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  exists: Boolean
+type EmailTemplateDocAccessFields_updatedAt_Update {
+  permission: Boolean!
 }
 
-input versionsEmailTemplate_version__emailSubjectTitle_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-  exists: Boolean
+type EmailTemplateDocAccessFields_updatedAt_Delete {
+  permission: Boolean!
 }
 
-input versionsEmailTemplate_version__emailTemplatesHandlebarsCode_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
+type EmailTemplateDocAccessFields_createdAt {
+  create: EmailTemplateDocAccessFields_createdAt_Create
+  read: EmailTemplateDocAccessFields_createdAt_Read
+  update: EmailTemplateDocAccessFields_createdAt_Update
+  delete: EmailTemplateDocAccessFields_createdAt_Delete
 }
 
-input versionsEmailTemplate_version__updatedAt_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
+type EmailTemplateDocAccessFields_createdAt_Create {
+  permission: Boolean!
 }
 
-input versionsEmailTemplate_version__createdAt_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
+type EmailTemplateDocAccessFields_createdAt_Read {
+  permission: Boolean!
 }
 
-input versionsEmailTemplate_version___status_operator {
-  equals: versionsEmailTemplate_version___status_Input
-  not_equals: versionsEmailTemplate_version___status_Input
-  in: [versionsEmailTemplate_version___status_Input]
-  not_in: [versionsEmailTemplate_version___status_Input]
-  all: [versionsEmailTemplate_version___status_Input]
-  exists: Boolean
+type EmailTemplateDocAccessFields_createdAt_Update {
+  permission: Boolean!
 }
 
-enum versionsEmailTemplate_version___status_Input {
-  draft
-  published
+type EmailTemplateDocAccessFields_createdAt_Delete {
+  permission: Boolean!
 }
 
-input versionsEmailTemplate_createdAt_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
+type EmailTemplateCreateDocAccess {
+  permission: Boolean!
+  where: JSONObject
 }
 
-input versionsEmailTemplate_updatedAt_operator {
-  equals: DateTime
-  not_equals: DateTime
-  greater_than_equal: DateTime
-  greater_than: DateTime
-  less_than_equal: DateTime
-  less_than: DateTime
-  like: DateTime
-  exists: Boolean
+type EmailTemplateReadDocAccess {
+  permission: Boolean!
+  where: JSONObject
 }
 
-input versionsEmailTemplate_id_operator {
-  equals: String
-  not_equals: String
-  like: String
-  contains: String
-  in: [String]
-  not_in: [String]
-  all: [String]
-  exists: Boolean
+type EmailTemplateUpdateDocAccess {
+  permission: Boolean!
+  where: JSONObject
 }
 
-input versionsEmailTemplate_where_or {
-  parent: versionsEmailTemplate_parent_operator
-  version__title: versionsEmailTemplate_version__title_operator
-  version__internalNotes: versionsEmailTemplate_version__internalNotes_operator
-  version__emailSubjectTitle: versionsEmailTemplate_version__emailSubjectTitle_operator
-  version__emailTemplatesHandlebarsCode: versionsEmailTemplate_version__emailTemplatesHandlebarsCode_operator
-  version__updatedAt: versionsEmailTemplate_version__updatedAt_operator
-  version__createdAt: versionsEmailTemplate_version__createdAt_operator
-  version___status: versionsEmailTemplate_version___status_operator
-  createdAt: versionsEmailTemplate_createdAt_operator
-  updatedAt: versionsEmailTemplate_updatedAt_operator
-  id: versionsEmailTemplate_id_operator
-}
-
-input versionsEmailTemplate_where_and {
-  parent: versionsEmailTemplate_parent_operator
-  version__title: versionsEmailTemplate_version__title_operator
-  version__internalNotes: versionsEmailTemplate_version__internalNotes_operator
-  version__emailSubjectTitle: versionsEmailTemplate_version__emailSubjectTitle_operator
-  version__emailTemplatesHandlebarsCode: versionsEmailTemplate_version__emailTemplatesHandlebarsCode_operator
-  version__updatedAt: versionsEmailTemplate_version__updatedAt_operator
-  version__createdAt: versionsEmailTemplate_version__createdAt_operator
-  version___status: versionsEmailTemplate_version___status_operator
-  createdAt: versionsEmailTemplate_createdAt_operator
-  updatedAt: versionsEmailTemplate_updatedAt_operator
-  id: versionsEmailTemplate_id_operator
+type EmailTemplateDeleteDocAccess {
+  permission: Boolean!
+  where: JSONObject
 }
 
 type Preference {
@@ -2980,6 +2306,8 @@ type usersAccess {
 
 type UsersFields {
   name: UsersFields_name
+  isAdmin: UsersFields_isAdmin
+  tenant: UsersFields_tenant
   updatedAt: UsersFields_updatedAt
   createdAt: UsersFields_createdAt
   email: UsersFields_email
@@ -3009,6 +2337,52 @@ type UsersFields_name_Delete {
   permission: Boolean!
 }
 
+type UsersFields_isAdmin {
+  create: UsersFields_isAdmin_Create
+  read: UsersFields_isAdmin_Read
+  update: UsersFields_isAdmin_Update
+  delete: UsersFields_isAdmin_Delete
+}
+
+type UsersFields_isAdmin_Create {
+  permission: Boolean!
+}
+
+type UsersFields_isAdmin_Read {
+  permission: Boolean!
+}
+
+type UsersFields_isAdmin_Update {
+  permission: Boolean!
+}
+
+type UsersFields_isAdmin_Delete {
+  permission: Boolean!
+}
+
+type UsersFields_tenant {
+  create: UsersFields_tenant_Create
+  read: UsersFields_tenant_Read
+  update: UsersFields_tenant_Update
+  delete: UsersFields_tenant_Delete
+}
+
+type UsersFields_tenant_Create {
+  permission: Boolean!
+}
+
+type UsersFields_tenant_Read {
+  permission: Boolean!
+}
+
+type UsersFields_tenant_Update {
+  permission: Boolean!
+}
+
+type UsersFields_tenant_Delete {
+  permission: Boolean!
+}
+
 type UsersFields_updatedAt {
   create: UsersFields_updatedAt_Create
   read: UsersFields_updatedAt_Read
@@ -3132,7 +2506,6 @@ type credential_templateAccess {
   read: CredentialTemplateReadAccess
   update: CredentialTemplateUpdateAccess
   delete: CredentialTemplateDeleteAccess
-  readVersions: CredentialTemplateReadVersionsAccess
 }
 
 type CredentialTemplateFields {
@@ -3142,7 +2515,6 @@ type CredentialTemplateFields {
   credentialTemplateJson: CredentialTemplateFields_credentialTemplateJson
   updatedAt: CredentialTemplateFields_updatedAt
   createdAt: CredentialTemplateFields_createdAt
-  _status: CredentialTemplateFields__status
 }
 
 type CredentialTemplateFields_title {
@@ -3283,29 +2655,6 @@ type CredentialTemplateFields_createdAt_Delete {
   permission: Boolean!
 }
 
-type CredentialTemplateFields__status {
-  create: CredentialTemplateFields__status_Create
-  read: CredentialTemplateFields__status_Read
-  update: CredentialTemplateFields__status_Update
-  delete: CredentialTemplateFields__status_Delete
-}
-
-type CredentialTemplateFields__status_Create {
-  permission: Boolean!
-}
-
-type CredentialTemplateFields__status_Read {
-  permission: Boolean!
-}
-
-type CredentialTemplateFields__status_Update {
-  permission: Boolean!
-}
-
-type CredentialTemplateFields__status_Delete {
-  permission: Boolean!
-}
-
 type CredentialTemplateCreateAccess {
   permission: Boolean!
   where: JSONObject
@@ -3326,11 +2675,6 @@ type CredentialTemplateDeleteAccess {
   where: JSONObject
 }
 
-type CredentialTemplateReadVersionsAccess {
-  permission: Boolean!
-  where: JSONObject
-}
-
 type credential_batchAccess {
   fields: CredentialBatchFields
   create: CredentialBatchCreateAccess
@@ -3347,6 +2691,7 @@ type CredentialBatchFields {
   status: CredentialBatchFields_status
   template: CredentialBatchFields_template
   emailTemplate: CredentialBatchFields_emailTemplate
+  from: CredentialBatchFields_from
   csvFields: CredentialBatchFields_csvFields
   updatedAt: CredentialBatchFields_updatedAt
   createdAt: CredentialBatchFields_createdAt
@@ -3491,6 +2836,29 @@ type CredentialBatchFields_emailTemplate_Delete {
   permission: Boolean!
 }
 
+type CredentialBatchFields_from {
+  create: CredentialBatchFields_from_Create
+  read: CredentialBatchFields_from_Read
+  update: CredentialBatchFields_from_Update
+  delete: CredentialBatchFields_from_Delete
+}
+
+type CredentialBatchFields_from_Create {
+  permission: Boolean!
+}
+
+type CredentialBatchFields_from_Read {
+  permission: Boolean!
+}
+
+type CredentialBatchFields_from_Update {
+  permission: Boolean!
+}
+
+type CredentialBatchFields_from_Delete {
+  permission: Boolean!
+}
+
 type CredentialBatchFields_csvFields {
   create: CredentialBatchFields_csvFields_Create
   read: CredentialBatchFields_csvFields_Read
@@ -3614,7 +2982,6 @@ type credentialAccess {
   read: CredentialReadAccess
   update: CredentialUpdateAccess
   delete: CredentialDeleteAccess
-  readVersions: CredentialReadVersionsAccess
 }
 
 type CredentialFields {
@@ -3630,7 +2997,6 @@ type CredentialFields {
   actionButton: CredentialFields_actionButton
   updatedAt: CredentialFields_updatedAt
   createdAt: CredentialFields_createdAt
-  _status: CredentialFields__status
 }
 
 type CredentialFields_credentialName {
@@ -3909,29 +3275,6 @@ type CredentialFields_createdAt_Delete {
   permission: Boolean!
 }
 
-type CredentialFields__status {
-  create: CredentialFields__status_Create
-  read: CredentialFields__status_Read
-  update: CredentialFields__status_Update
-  delete: CredentialFields__status_Delete
-}
-
-type CredentialFields__status_Create {
-  permission: Boolean!
-}
-
-type CredentialFields__status_Read {
-  permission: Boolean!
-}
-
-type CredentialFields__status_Update {
-  permission: Boolean!
-}
-
-type CredentialFields__status_Delete {
-  permission: Boolean!
-}
-
 type CredentialCreateAccess {
   permission: Boolean!
   where: JSONObject
@@ -3952,18 +3295,12 @@ type CredentialDeleteAccess {
   where: JSONObject
 }
 
-type CredentialReadVersionsAccess {
-  permission: Boolean!
-  where: JSONObject
-}
-
 type email_templateAccess {
   fields: EmailTemplateFields
   create: EmailTemplateCreateAccess
   read: EmailTemplateReadAccess
   update: EmailTemplateUpdateAccess
   delete: EmailTemplateDeleteAccess
-  readVersions: EmailTemplateReadVersionsAccess
 }
 
 type EmailTemplateFields {
@@ -3973,7 +3310,6 @@ type EmailTemplateFields {
   emailTemplatesHandlebarsCode: EmailTemplateFields_emailTemplatesHandlebarsCode
   updatedAt: EmailTemplateFields_updatedAt
   createdAt: EmailTemplateFields_createdAt
-  _status: EmailTemplateFields__status
 }
 
 type EmailTemplateFields_title {
@@ -4114,29 +3450,6 @@ type EmailTemplateFields_createdAt_Delete {
   permission: Boolean!
 }
 
-type EmailTemplateFields__status {
-  create: EmailTemplateFields__status_Create
-  read: EmailTemplateFields__status_Read
-  update: EmailTemplateFields__status_Update
-  delete: EmailTemplateFields__status_Delete
-}
-
-type EmailTemplateFields__status_Create {
-  permission: Boolean!
-}
-
-type EmailTemplateFields__status_Read {
-  permission: Boolean!
-}
-
-type EmailTemplateFields__status_Update {
-  permission: Boolean!
-}
-
-type EmailTemplateFields__status_Delete {
-  permission: Boolean!
-}
-
 type EmailTemplateCreateAccess {
   permission: Boolean!
   where: JSONObject
@@ -4157,11 +3470,6 @@ type EmailTemplateDeleteAccess {
   where: JSONObject
 }
 
-type EmailTemplateReadVersionsAccess {
-  permission: Boolean!
-  where: JSONObject
-}
-
 type Mutation {
   createUser(data: mutationUserInput!, draft: Boolean): User
   updateUser(id: String!, data: mutationUserUpdateInput!, draft: Boolean, autosave: Boolean): User
@@ -4176,7 +3484,6 @@ type Mutation {
   createCredentialTemplate(data: mutationCredentialTemplateInput!, draft: Boolean): CredentialTemplate
   updateCredentialTemplate(id: String!, data: mutationCredentialTemplateUpdateInput!, draft: Boolean, autosave: Boolean): CredentialTemplate
   deleteCredentialTemplate(id: String!): CredentialTemplate
-  restoreVersionCredentialTemplate(id: String): CredentialTemplate
   createCredentialBatch(data: mutationCredentialBatchInput!, draft: Boolean): CredentialBatch
   updateCredentialBatch(id: String!, data: mutationCredentialBatchUpdateInput!, draft: Boolean, autosave: Boolean): CredentialBatch
   deleteCredentialBatch(id: String!): CredentialBatch
@@ -4184,17 +3491,17 @@ type Mutation {
   createCredential(data: mutationCredentialInput!, draft: Boolean): Credential
   updateCredential(id: String!, data: mutationCredentialUpdateInput!, draft: Boolean, autosave: Boolean): Credential
   deleteCredential(id: String!): Credential
-  restoreVersionCredential(id: String): Credential
   createEmailTemplate(data: mutationEmailTemplateInput!, draft: Boolean): EmailTemplate
   updateEmailTemplate(id: String!, data: mutationEmailTemplateUpdateInput!, draft: Boolean, autosave: Boolean): EmailTemplate
   deleteEmailTemplate(id: String!): EmailTemplate
-  restoreVersionEmailTemplate(id: String): EmailTemplate
   updatePreference(key: String!, value: JSON): Preference
   deletePreference(key: String!): Preference
 }
 
 input mutationUserInput {
   name: String
+  isAdmin: Boolean
+  tenant: String
   updatedAt: String
   createdAt: String
   email: String!
@@ -4209,6 +3516,8 @@ input mutationUserInput {
 
 input mutationUserUpdateInput {
   name: String
+  isAdmin: Boolean
+  tenant: String
   updatedAt: String
   createdAt: String
   email: String
@@ -4228,6 +3537,8 @@ type usersRefreshedUser {
 }
 
 type usersJWT {
+  isAdmin: Boolean
+  tenant: String
   email: EmailAddress!
   collection: String!
 }
@@ -4244,42 +3555,34 @@ type usersResetPassword {
 }
 
 input mutationCredentialTemplateInput {
+  tenant: String
   title: String!
   description: String
   internalNotes: String
   credentialTemplateJson: JSON!
   updatedAt: String
   createdAt: String
-  _status: CredentialTemplate__status_MutationInput
-}
-
-enum CredentialTemplate__status_MutationInput {
-  draft
-  published
 }
 
 input mutationCredentialTemplateUpdateInput {
+  tenant: String
   title: String
   description: String
   internalNotes: String
   credentialTemplateJson: JSON
   updatedAt: String
   createdAt: String
-  _status: CredentialTemplateUpdate__status_MutationInput
-}
-
-enum CredentialTemplateUpdate__status_MutationInput {
-  draft
-  published
 }
 
 input mutationCredentialBatchInput {
+  tenant: String
   title: String!
   description: String
   internalNotes: String
   status: String!
   template: String
   emailTemplate: String
+  from: String
   csvFields: JSON
   updatedAt: String
   createdAt: String
@@ -4292,12 +3595,14 @@ enum CredentialBatch__status_MutationInput {
 }
 
 input mutationCredentialBatchUpdateInput {
+  tenant: String
   title: String
   description: String
   internalNotes: String
   status: String
   template: String
   emailTemplate: String
+  from: String
   csvFields: JSON
   updatedAt: String
   createdAt: String
@@ -4310,6 +3615,7 @@ enum CredentialBatchUpdate__status_MutationInput {
 }
 
 input mutationCredentialInput {
+  tenant: String
   credentialName: String
   earnerName: String
   emailAddress: String
@@ -4321,15 +3627,10 @@ input mutationCredentialInput {
   revokedBy: String
   updatedAt: String
   createdAt: String
-  _status: Credential__status_MutationInput
-}
-
-enum Credential__status_MutationInput {
-  draft
-  published
 }
 
 input mutationCredentialUpdateInput {
+  tenant: String
   credentialName: String
   earnerName: String
   emailAddress: String
@@ -4341,40 +3642,26 @@ input mutationCredentialUpdateInput {
   revokedBy: String
   updatedAt: String
   createdAt: String
-  _status: CredentialUpdate__status_MutationInput
-}
-
-enum CredentialUpdate__status_MutationInput {
-  draft
-  published
 }
 
 input mutationEmailTemplateInput {
+  tenant: String
   title: String!
   internalNotes: String
+  from: String
   emailSubjectTitle: String
   emailTemplatesHandlebarsCode: String!
   updatedAt: String
   createdAt: String
-  _status: EmailTemplate__status_MutationInput
-}
-
-enum EmailTemplate__status_MutationInput {
-  draft
-  published
 }
 
 input mutationEmailTemplateUpdateInput {
+  tenant: String
   title: String
   internalNotes: String
+  from: String
   emailSubjectTitle: String
   emailTemplatesHandlebarsCode: String
   updatedAt: String
   createdAt: String
-  _status: EmailTemplateUpdate__status_MutationInput
-}
-
-enum EmailTemplateUpdate__status_MutationInput {
-  draft
-  published
 }
\ No newline at end of file
diff --git a/src/helpers/tenant.ts b/src/helpers/tenant.ts
new file mode 100644
index 0000000..cea7ee7
--- /dev/null
+++ b/src/helpers/tenant.ts
@@ -0,0 +1,21 @@
+import { Access, CollectionBeforeChangeHook } from 'payload/types';
+
+
+// when set on read permission filters all payload.find() and payload.findById()
+export const tenantAccessFilterQuery: Access = ({ req }) => {
+  return {
+    tenant: {
+      equals: req.headers.host,
+    }
+  }
+}
+
+export const setTenantOnCreate: CollectionBeforeChangeHook =
+  ({ data, req, operation, originalDoc }) => {
+    if (operation === "create") {
+      data["tenant"] = req.headers.host;
+      return data;
+    }
+    return originalDoc;
+  }
+
diff --git a/src/payload-types.ts b/src/payload-types.ts
index 0904810..0546397 100644
--- a/src/payload-types.ts
+++ b/src/payload-types.ts
@@ -19,6 +19,8 @@ export interface Config {
 export interface User {
   id: string;
   name?: string;
+  isAdmin?: boolean;
+  tenant?: string;
   updatedAt: string;
   createdAt: string;
   email: string;
@@ -32,6 +34,7 @@ export interface User {
 }
 export interface CredentialTemplate {
   id: string;
+  tenant?: string;
   title: string;
   description?: string;
   internalNotes?: string;
@@ -46,10 +49,10 @@ export interface CredentialTemplate {
     | null;
   updatedAt: string;
   createdAt: string;
-  _status?: 'draft' | 'published';
 }
 export interface CredentialBatch {
   id: string;
+  tenant?: string;
   title: string;
   description?: string;
   internalNotes?: string;
@@ -72,6 +75,7 @@ export interface CredentialBatch {
 }
 export interface EmailTemplate {
   id: string;
+  tenant?: string;
   title: string;
   internalNotes?: string;
   from?: string;
@@ -79,10 +83,10 @@ export interface EmailTemplate {
   emailTemplatesHandlebarsCode: string;
   updatedAt: string;
   createdAt: string;
-  _status?: 'draft' | 'published';
 }
 export interface Credential {
   id: string;
+  tenant?: string;
   credentialName?: string;
   earnerName?: string;
   emailAddress?: string;
diff --git a/src/payload.config.ts b/src/payload.config.ts
index fe68db2..3bc5463 100644
--- a/src/payload.config.ts
+++ b/src/payload.config.ts
@@ -47,7 +47,7 @@ export default buildConfig({
         fromName: 'DCC',
         fromAddress: 'chartraj@mit.edu',
     },
-    serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL || 'http://localhost:3000',
+    serverURL: "",
     cors: '*',
     admin: {
         css: require.resolve('./components/global.scss'),
@@ -111,7 +111,7 @@ export default buildConfig({
         { method: 'post', path: '/get-collection-count', handler: getCollectionCount },
         { method: 'get', path: '/get-credential-links', handler: getCredentialLinks },
         { method: 'post', path: '/exchange/:a/:b/:token', handler: forwardExchangeRequest },
-        { method: 'post', path: '/revoke-credential/:id', handler: revokeCredential },
+        // { method: 'post', path: '/revoke-credential/:id', handler: revokeCredential },
     ],
     typescript: {
         outputFile: path.resolve(__dirname, 'payload-types.ts'),
@@ -119,4 +119,9 @@ export default buildConfig({
     graphQL: {
         schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
     },
+    express: {
+  		json: {
+  			limit: 1024 * 4,
+  		}
+	}
 });
diff --git a/src/server.ts b/src/server.ts
index 4c5c6cd..1d639fd 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,6 +1,7 @@
 import express from "express";
 import payload from "payload";
 import path from "path";
+import jwt from 'jsonwebtoken';
 
 require("dotenv").config();
 const app = express();
@@ -11,6 +12,27 @@ app.get("/", (_, res) => {
   res.redirect("/admin");
 });
 
+app.use((req, res, next) => {
+  const cookieHeader = req.headers.cookie || '';
+  const tokenMatch = cookieHeader.match(/payload-token=([^;]+)/);
+
+  if (tokenMatch) {
+    const { tenant, isAdmin } = jwt.decode(tokenMatch[1]) as any;
+
+    if (isAdmin) {
+      console.log("admin accessed tenant: ", tenant)
+      return next()
+    }
+
+    // check if user is on wrong domain
+    if (tenant !== req.headers.host) {
+      res.clearCookie('payload-token');
+      return res.sendStatus(401);
+    }
+  }
+  return next();
+})
+
 const start = async () => {
   // Initialize Payload
   await payload.init({


Reviews:
user: login: nqthqn
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-01T20:58:59Z
user: login: nqthqn
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-01T21:01:23Z
user: login: nqthqn
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-02T22:24:34Z
user: login: nqthqn
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-02T22:24:57Z
user: login: nqthqn
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-02T22:26:39Z
user: login: nqthqn
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-02T22:27:28Z
user: login: nqthqn
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-02T22:37:25Z
user: login: nqthqn
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-02T22:38:10Z
user: login: nqthqn
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-02T22:38:43Z
user: login: ottonomy
body: This looks good. One change I'd like to see is a log message documenting admin access to a tenant-specific domain. 

Also want to discuss briefly the user collection stories, see comment.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-14T16:28:08Z
user: login: nqthqn
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-14T16:42:09Z
user: login: nqthqn
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-14T17:05:17Z
user: login: nqthqn
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-22T19:51:57Z

Review comments:
diff_hunk: @@ -46,7 +48,6 @@ export interface CredentialTemplate {
     | null;
   updatedAt: string;
   createdAt: string;
-  _status?: 'draft' | 'published';
path: src/payload-types.ts
user: login: nqthqn
body: I'm not sure why these got removed from the generated types file
created_at: 2025-04-01T20:58:58Z
author_association: NONE
diff_hunk: @@ -11,6 +12,22 @@ app.get("/", (_, res) => {
   res.redirect("/admin");
 });
 
+app.use((req, res, next) => {
+  const cookieHeader = req.headers.cookie || '';
+  const tokenMatch = cookieHeader.match(/payload-token=([^;]+)/);
+  if (tokenMatch) {
+    const { tenant, roles } = jwt.decode(tokenMatch[1]) as any;
path: src/server.ts
user: login: nqthqn
body: Assuming payloadjs handles verification — on every request I just want a peek inside the payload.
created_at: 2025-04-01T21:01:23Z
author_association: NONE
diff_hunk: @@ -1,17 +1,54 @@
 import { CollectionConfig } from 'payload/types';
 import UserPageDescription from '../components/User/UserPageDescription';
 import CreateUser from '../components/User/CreateUser';
+import { tenantAccessFilterQuery, setTenantOnCreate } from '../helpers/tenant';
+
+const isAdmin = ({ req }) => req.user.isAdmin;
 
 const Users: CollectionConfig = {
-    slug: 'users',
-    auth: { tokenExpiration: 7 * 24 * 60 * 60 },
-    admin: {
-        useAsTitle: 'name',
-        defaultColumns: ['name', 'email'],
-        description: UserPageDescription,
-        components: { views: { Edit: CreateUser } },
+  slug: 'users',
+  auth: { tokenExpiration: 7 * 24 * 60 * 60 },
+  admin: {
+    useAsTitle: 'name',
+    defaultColumns: ['name', 'email'],
+    description: UserPageDescription,
+    components: { views: { Edit: CreateUser } },
+  },
+  access: {
+    read: (args) => isAdmin(args) ?
+      tenantAccessFilterQuery(args) : { id: { equals: args.req.user.id } },
path: src/collections/Users.ts
user: login: nqthqn
body: This means admins can see list of users filtered by current tenant domain and normal users can only see their own user data
created_at: 2025-04-02T22:24:34Z
author_association: NONE
diff_hunk: @@ -1,17 +1,54 @@
 import { CollectionConfig } from 'payload/types';
 import UserPageDescription from '../components/User/UserPageDescription';
 import CreateUser from '../components/User/CreateUser';
+import { tenantAccessFilterQuery, setTenantOnCreate } from '../helpers/tenant';
+
+const isAdmin = ({ req }) => req.user.isAdmin;
 
 const Users: CollectionConfig = {
-    slug: 'users',
-    auth: { tokenExpiration: 7 * 24 * 60 * 60 },
-    admin: {
-        useAsTitle: 'name',
-        defaultColumns: ['name', 'email'],
-        description: UserPageDescription,
-        components: { views: { Edit: CreateUser } },
+  slug: 'users',
+  auth: { tokenExpiration: 7 * 24 * 60 * 60 },
+  admin: {
+    useAsTitle: 'name',
+    defaultColumns: ['name', 'email'],
+    description: UserPageDescription,
+    components: { views: { Edit: CreateUser } },
+  },
+  access: {
+    read: (args) => isAdmin(args) ?
+      tenantAccessFilterQuery(args) : { id: { equals: args.req.user.id } },
+    create: (args) => isAdmin(args) ?
+      tenantAccessFilterQuery(args) : false,
path: src/collections/Users.ts
user: login: nqthqn
body: This means only admins can create users
created_at: 2025-04-02T22:24:57Z
author_association: NONE
diff_hunk: @@ -6,6 +6,7 @@ export const getCollectionCount: PayloadHandler = async (req, res) => {
     if (!req.user) return res.sendStatus(401);
     const collectionName = req?.body?.collectionName;
     const payloadQuery = req?.body?.query;
+    payloadQuery["tenant"] = req.headers.host;
path: src/endpoints/getCollectionCount.ts
user: login: nqthqn
body: This count operation does not obey access rules on `read` (which only applies to `find` and `findById` therefore server side we add this filter
created_at: 2025-04-02T22:26:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,21 @@
+import { Access, CollectionBeforeChangeHook } from 'payload/types';
+
+
+// when set on read permission filters all payload.find() and payload.findById()
+export const tenantAccessFilterQuery: Access = ({ req }) => {
+  return {
+    tenant: {
+      equals: req.headers.host,
+    }
+  }
+}
+
+export const setTenantOnCreate: CollectionBeforeChangeHook =
+  ({ data, req, operation, originalDoc }) => {
+    if (operation === "create") {
+      data["tenant"] = req.headers.host;
path: src/helpers/tenant.ts
user: login: nqthqn
body: We handle `user.tenant !== req.headers.host` in `server.ts`

created_at: 2025-04-02T22:27:28Z
author_association: NONE
diff_hunk: @@ -47,7 +47,7 @@ export default buildConfig({
         fromName: 'DCC',
         fromAddress: 'chartraj@mit.edu',
     },
-    serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL || 'http://localhost:3000',
+    serverURL: "",
path: src/payload.config.ts
user: login: nqthqn
body: This may have ramifications for claim flow
created_at: 2025-04-02T22:37:25Z
author_association: NONE
diff_hunk: @@ -119,4 +119,9 @@ export default buildConfig({
     graphQL: {
         schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
     },
+    express: {
+  		json: {
+  			limit: 1024 * 4,
path: src/payload.config.ts
user: login: nqthqn
body: Increases default request payload body size. Snuck into this PR :/
created_at: 2025-04-02T22:38:10Z
author_association: NONE
diff_hunk: @@ -11,6 +12,26 @@ app.get("/", (_, res) => {
   res.redirect("/admin");
 });
 
+app.use((req, res, next) => {
+  const cookieHeader = req.headers.cookie || '';
+  const tokenMatch = cookieHeader.match(/payload-token=([^;]+)/);
+
+  if (tokenMatch) {
+    const { tenant, isAdmin } = jwt.decode(tokenMatch[1]) as any;
path: src/server.ts
user: login: nqthqn
body: We don't verify jwt since payload handles this.
created_at: 2025-04-02T22:38:43Z
author_association: NONE
diff_hunk: @@ -1,17 +1,54 @@
 import { CollectionConfig } from 'payload/types';
 import UserPageDescription from '../components/User/UserPageDescription';
 import CreateUser from '../components/User/CreateUser';
+import { tenantAccessFilterQuery, setTenantOnCreate } from '../helpers/tenant';
+
+const isAdmin = ({ req }) => req.user.isAdmin;
 
 const Users: CollectionConfig = {
-    slug: 'users',
-    auth: { tokenExpiration: 7 * 24 * 60 * 60 },
-    admin: {
-        useAsTitle: 'name',
-        defaultColumns: ['name', 'email'],
-        description: UserPageDescription,
-        components: { views: { Edit: CreateUser } },
+  slug: 'users',
+  auth: { tokenExpiration: 7 * 24 * 60 * 60 },
+  admin: {
+    useAsTitle: 'name',
+    defaultColumns: ['name', 'email'],
+    description: UserPageDescription,
+    components: { views: { Edit: CreateUser } },
+  },
+  access: {
+    read: (args) => isAdmin(args) ?
+      tenantAccessFilterQuery(args) : { id: { equals: args.req.user.id } },
path: src/collections/Users.ts
user: login: ottonomy
body: Noting this. This is pretty close, but maybe we can improve it a little.

I think the optimal set of capabilities would be:
* Admin can see any user on this tenant domain (read/write privileges)
* Normal user can see and modify self (read/write)
* Normal user can see other users on the tenant domain (read-only) but cannot set their password.
created_at: 2025-04-14T16:06:38Z
author_association: NONE
diff_hunk: @@ -58,7 +57,7 @@ export const getCredentialLinks: PayloadHandler = async (req, res) => {
             headers: { 'Content-Type': 'application/json' },
             body: JSON.stringify({
                 data: [{ vc: builtCredential, retrievalId: id }],
-                tenantName,
+                tenantName: credential.tenant,
path: src/endpoints/getCredentialLinks.ts
user: login: ottonomy
body: Do we need to have any flexibility to assign a different tenant name other than the hostname for the admin dashboard deployment?

Are there good reasons why a deployment would not want to use names compatible with the approach?
* Would require character escaping or oddities in setting env vars
* Already have existing tenant names set up in the workflow coordinator that would be a pain to change?
created_at: 2025-04-14T16:17:00Z
author_association: NONE
diff_hunk: @@ -47,7 +47,7 @@ export default buildConfig({
         fromName: 'DCC',
         fromAddress: 'chartraj@mit.edu',
     },
-    serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL || 'http://localhost:3000',
+    serverURL: "",
path: src/payload.config.ts
user: login: ottonomy
body: Ok, let's talk through the claim flow, how the claim page, which is also operated in the context of a single tenant knows how to reach out to the right admin dashboard domain.
created_at: 2025-04-14T16:20:05Z
author_association: NONE
diff_hunk: @@ -119,4 +119,9 @@ export default buildConfig({
     graphQL: {
         schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
     },
+    express: {
+  		json: {
+  			limit: 1024 * 4,
path: src/payload.config.ts
user: login: ottonomy
body: :+1: I think we will need this in any case.
created_at: 2025-04-14T16:20:34Z
author_association: NONE
diff_hunk: @@ -11,6 +12,26 @@ app.get("/", (_, res) => {
   res.redirect("/admin");
 });
 
+app.use((req, res, next) => {
+  const cookieHeader = req.headers.cookie || '';
+  const tokenMatch = cookieHeader.match(/payload-token=([^;]+)/);
+
+  if (tokenMatch) {
+    const { tenant, isAdmin } = jwt.decode(tokenMatch[1]) as any;
+
+    if (isAdmin) {
+      return next()
path: src/server.ts
user: login: ottonomy
body: I would like to make a log statement for auditing showing that an admin accessed a particular tenant domain and path.
created_at: 2025-04-14T16:27:36Z
author_association: NONE
diff_hunk: @@ -11,6 +12,26 @@ app.get("/", (_, res) => {
   res.redirect("/admin");
 });
 
+app.use((req, res, next) => {
+  const cookieHeader = req.headers.cookie || '';
+  const tokenMatch = cookieHeader.match(/payload-token=([^;]+)/);
+
+  if (tokenMatch) {
+    const { tenant, isAdmin } = jwt.decode(tokenMatch[1]) as any;
+
+    if (isAdmin) {
+      return next()
path: src/server.ts
user: login: nqthqn
body: ```suggestion
      console.log("admin accessed: ", tenant);
      return next()
```
created_at: 2025-04-14T16:42:09Z
author_association: NONE
diff_hunk: @@ -1,17 +1,54 @@
 import { CollectionConfig } from 'payload/types';
 import UserPageDescription from '../components/User/UserPageDescription';
 import CreateUser from '../components/User/CreateUser';
+import { tenantAccessFilterQuery, setTenantOnCreate } from '../helpers/tenant';
+
+const isAdmin = ({ req }) => req.user.isAdmin;
 
 const Users: CollectionConfig = {
-    slug: 'users',
-    auth: { tokenExpiration: 7 * 24 * 60 * 60 },
-    admin: {
-        useAsTitle: 'name',
-        defaultColumns: ['name', 'email'],
-        description: UserPageDescription,
-        components: { views: { Edit: CreateUser } },
+  slug: 'users',
+  auth: { tokenExpiration: 7 * 24 * 60 * 60 },
+  admin: {
+    useAsTitle: 'name',
+    defaultColumns: ['name', 'email'],
+    description: UserPageDescription,
+    components: { views: { Edit: CreateUser } },
+  },
+  access: {
+    read: (args) => isAdmin(args) ?
+      tenantAccessFilterQuery(args) : { id: { equals: args.req.user.id } },
path: src/collections/Users.ts
user: login: nqthqn
body: ✅  Admin can see any user on this tenant domain (read/write privileges)
✅ Normal user can see and modify self (read/write)
❌  Normal user can see other users on the tenant domain (read-only) but cannot set their password.

I'll update the permissions for inner visibility without write

created_at: 2025-04-14T17:05:17Z
author_association: NONE
diff_hunk: @@ -58,7 +57,7 @@ export const getCredentialLinks: PayloadHandler = async (req, res) => {
             headers: { 'Content-Type': 'application/json' },
             body: JSON.stringify({
                 data: [{ vc: builtCredential, retrievalId: id }],
-                tenantName,
+                tenantName: credential.tenant,
path: src/endpoints/getCredentialLinks.ts
user: login: nqthqn
body: https://github.com/skybridgeskills/dcc-workflow-coordinator/pull/1/files
created_at: 2025-04-22T19:51:57Z
author_association: NONE

PR comments:
user: login: ottonomy
created_at: 2025-04-01T21:11:52Z
body: Architectural approach looks good and balanced.
author_association: NONE
user: login: nqthqn
created_at: 2025-04-02T21:52:16Z
body: Making it simpler

<img width="625" alt="image" src="https://github.com/user-attachments/assets/b0bb843f-acee-4e08-9894-bd7f2859000c" />

author_association: NONE