diff --git a/.dockerignore b/.dockerignore
index b99e7de969338ea4dab5eedd05299349aa8c7049..f5a5e249bfe45608d8d00a85d90918a0d9062ffc 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,3 +1,4 @@
 node_modules
 .git
 .gitignore
+data
diff --git a/.eslintignore b/.eslintignore
index ed4598e7fabda7e5f6950645c5570c66ac5cfd28..cff71ae1a6ce1e94e00f0ec28298bb3a56d89fc2 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,2 +1,3 @@
 node_modules
 bin
+data
diff --git a/.gitignore b/.gitignore
index 349f2b1937a1bcf134e2e996fd86b8945de07636..c9156895f6e72132faecaf379c297f032f3c43f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,6 @@ coverage
 .netlify
 
 kong.yml
+
+
+data
diff --git a/.prettierignore b/.prettierignore
index 6895bf07a960c02734f68bf0ef353f557ec1a7e1..161501f965cbb072852ab334a693a67d0a7e6aa4 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,3 +1,3 @@
 node_modules
 coverage
-
+data
diff --git a/README.md b/README.md
index 40e017d78154d9569ff37fb1ff4ec42c51d99bcc..bc73802b541ec12a8f6d72ce7308359449343498 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# RESTful API Node Server Boilerplate
+# RESTful API Node Server Boilerplate - te
 
 [![Build Status](https://travis-ci.org/hagopj13/node-express-boilerplate.svg?branch=master)](https://travis-ci.org/hagopj13/node-express-boilerplate)
 [![Coverage Status](https://coveralls.io/repos/github/hagopj13/node-express-boilerplate/badge.svg?branch=master)](https://coveralls.io/github/hagopj13/node-express-boilerplate?branch=master)
diff --git a/docker-compose.yml b/docker-compose.yml
index 65a108e4be87a423e686e612d6e40081f376af1b..26dcb0a5dcc1c133cc1bdeea7776efd2596f6240 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -36,8 +36,6 @@ services:
   playground-db:
     container_name: ${DB_CONTAINER_NAME:-playground-db}
     image: mongo:4.4.6-bionic
-    ports:
-      - '${MONGO_EXPOSE_PORT}:27017'
     volumes:
       - dbdata:/data/db
     networks:
@@ -47,8 +45,9 @@ services:
   upload:
     platform: linux/amd64
     container_name: ${ENV:-dev}-upload
-    build: ./upload/
-    image: boschvn/upload:${IMAGE_TAG:-latest}
+    image: boschvn/upload:latest
+    env_file:
+      - .env
     volumes:
       - '${UPLOAD_PATH}:/usr/src/upload/data'
     networks:
diff --git a/kong.yml.template b/kong.yml.template
index 657cfb6d9104855585501412acd00f42b7739d15..1a60994422ecfd383608083258acce58aa5dcb3d 100644
--- a/kong.yml.template
+++ b/kong.yml.template
@@ -18,6 +18,7 @@ services:
           - HEAD
           - CONNECT
           - TRACE
+    read_timeout: 180000
 plugins:
   - name: rate-limiting
     enabled: true
@@ -29,7 +30,7 @@ plugins:
     config:
       fault_tolerant: true
       hide_client_headers: false
-      minute: 60
+      minute: 120
       limit_by: header
       header_name: x-forwarded-for
       policy: local
diff --git a/package.json b/package.json
index 9aecb60654f97dd31c7862df8d2d5f38675ac212..ba4e2736e19b01b6dbb250d91913e0c34ecc1334 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
   },
   "scripts": {
     "start": "pm2 start ecosystem.config.json --no-daemon",
-    "dev": "cross-env NODE_ENV=development nodemon src/index.js",
+    "dev": "cross-env NODE_ENV=development nodemon --ignore 'data/*' src/index.js",
     "test": "jest -i --colors --verbose --detectOpenHandles",
     "test:watch": "jest -i --watchAll",
     "coverage": "jest -i --coverage",
@@ -69,6 +69,7 @@
     "jimp": "^0.22.12",
     "joi": "^17.3.0",
     "jsonwebtoken": "^8.5.1",
+    "lodash": "^4.17.21",
     "moment": "^2.24.0",
     "mongoose": "^5.7.7",
     "morgan": "^1.9.1",
@@ -76,15 +77,16 @@
     "passport": "^0.4.0",
     "passport-jwt": "^4.0.0",
     "pm2": "^5.1.0",
-    "socket.io": "^4.7.5",
+    "socket.io": "^4.8.0",
     "swagger-jsdoc": "^6.0.8",
     "swagger-ui-express": "^4.1.6",
     "utf-8-validate": "^6.0.4",
     "validator": "^13.0.0",
-    "winston": "^3.2.1",
-    "xss-clean": "^0.1.1"
+    "websocket": "^1.0.35",
+    "winston": "^3.2.1"
   },
   "devDependencies": {
+    "@types/lodash": "^4.17.12",
     "@types/mongoose": "^5.11.97",
     "coveralls": "^3.0.7",
     "eslint": "^7.0.0",
diff --git a/src/app.js b/src/app.js
index 97a3c4eae5a320a6f18d0419e22287d3158a5ec5..947ff0cdec5bfdf1b966cbaff08fce44927e0138 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1,6 +1,5 @@
 const express = require('express');
 const helmet = require('helmet');
-const xss = require('xss-clean');
 const cookies = require('cookie-parser');
 const mongoSanitize = require('express-mongo-sanitize');
 const compression = require('compression');
@@ -16,6 +15,7 @@ const routesV2 = require('./routes/v2');
 const { errorConverter, errorHandler } = require('./middlewares/error');
 const ApiError = require('./utils/ApiError');
 const setupProxy = require('./config/proxyHandler');
+const { init: initSocketIO } = require('./config/socket');
 
 const app = express();
 
@@ -37,7 +37,6 @@ app.use(express.json({ limit: '50mb', strict: false }));
 app.use(express.urlencoded({ extended: true, limit: '50mb', parameterLimit: 10000 }));
 
 // sanitize request data
-app.use(xss());
 app.use(mongoSanitize());
 
 // gzip compression
@@ -46,14 +45,7 @@ app.use(compression());
 // enable cors
 app.use(
   cors({
-    origin: [
-      /localhost:\d+/,
-      /\.digitalauto\.tech$/,
-      /\.digitalauto\.asia$/,
-      /\.digital\.auto$/,
-      'https://digitalauto.netlify.app',
-      /127\.0\.0\.1:\d+/,
-    ],
+    origin: config.cors.regex,
     credentials: true,
   })
 );
@@ -63,18 +55,14 @@ app.options('*', cors());
 app.use(passport.initialize());
 passport.use('jwt', jwtStrategy);
 
-// limit repeated failed requests to auth endpoints
-// if (config.env === 'production') {
-//  app.use('/v1/auth', authLimiter);
-//  app.use('/v2/auth', authLimiter);
-// }
-
 // v1 api routes
 app.use('/v1', routes);
 app.use('/v2', routesV2);
 
 // Setup proxy to other services
 setupProxy(app);
+const server = require('http').createServer(app);
+initSocketIO(server);
 
 // send back a 404 error for any unknown api request
 app.use((req, res, next) => {
diff --git a/src/config/config.js b/src/config/config.js
index eddc37db4241d2307c176f9be4f104ce7713f67d..49739f25bc61d7d17b3820749fb3ed38ab1b726d 100644
--- a/src/config/config.js
+++ b/src/config/config.js
@@ -18,6 +18,8 @@ const envVarsSchema = Joi.object()
     JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: Joi.number()
       .default(10)
       .description('minutes after which verify email token expires'),
+    JWT_COOKIE_NAME: Joi.string().required().description('JWT cookie name'),
+    JWT_COOKIE_DOMAIN: Joi.string().required().description('JWT cookie domain'),
     SMTP_HOST: Joi.string().description('server that will send the emails'),
     SMTP_PORT: Joi.number().description('port to connect to the email server'),
     SMTP_USERNAME: Joi.string().description('username for email server'),
@@ -45,6 +47,7 @@ const envVarsSchema = Joi.object()
     ETAS_CLIENT_SECRET: Joi.string().description('ETAS client secret'),
     ETAS_SCOPE: Joi.string().description('ETAS scope'),
     ETAS_INSTANCE_ENDPOINT: Joi.string().description('ETAS instance endpoint'),
+    ETAS_DEV_INSTANCE_ENDPOINT: Joi.string().description('ETAS dev instance endpoint'),
     // Certivity
     CERTIVITY_CLIENT_ID: Joi.string().required().description('Certivity client id'),
     CERTIVITY_CLIENT_SECRET: Joi.string().required().description('Certivity client secret'),
@@ -62,6 +65,16 @@ const config = {
   env: envVars.NODE_ENV,
   port: envVars.PORT,
   strictAuth: envVars.STRICT_AUTH,
+  cors: {
+    regex: [
+      /localhost:\d+/,
+      /\.digitalauto\.tech$/,
+      /\.digitalauto\.asia$/,
+      /\.digital\.auto$/,
+      'https://digitalauto.netlify.app',
+      /127\.0\.0\.1:\d+/,
+    ],
+  },
   mongoose: {
     url: envVars.MONGODB_URL + (envVars.NODE_ENV === 'test' ? '-test' : ''),
     options: {
@@ -77,10 +90,14 @@ const config = {
     refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS,
     resetPasswordExpirationMinutes: envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES,
     verifyEmailExpirationMinutes: envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES,
-    cookieRefreshOptions: {
-      secure: true,
-      httpOnly: true,
-      sameSite: 'None',
+    cookie: {
+      name: envVars.JWT_COOKIE_NAME,
+      options: {
+        secure: true,
+        httpOnly: true,
+        sameSite: 'None',
+        ...(envVars.NODE_ENV === 'production' && { domain: envVars.JWT_COOKIE_DOMAIN }),
+      },
     },
   },
   email: {
@@ -140,6 +157,7 @@ const config = {
     clientSecret: envVars.ETAS_CLIENT_SECRET,
     scope: envVars.ETAS_SCOPE,
     instanceEndpoint: envVars.ETAS_INSTANCE_ENDPOINT,
+    developmentEndpoint: envVars.ETAS_DEV_INSTANCE_ENDPOINT,
   },
   githubIssueSubmitUrl: 'https://api.github.com/repos/digital-auto/vehicle_signal_specification/issues',
   certivity: {
diff --git a/src/config/passport.js b/src/config/passport.js
index d92ebf346bf726f3ea2187f77cc586589bb42b03..73feed0d02550af82781b415828e24de3c1906d8 100644
--- a/src/config/passport.js
+++ b/src/config/passport.js
@@ -31,4 +31,5 @@ const jwtStrategy = new JwtStrategy(jwtOptions, jwtVerify);
 
 module.exports = {
   jwtStrategy,
+  jwtVerify,
 };
diff --git a/src/config/roles.js b/src/config/roles.js
index 227987ee5dc1d726bb24c9e2d8f0c457f4578731..9abcebf755118802ef43b43d7aed52a024ad3cb7 100644
--- a/src/config/roles.js
+++ b/src/config/roles.js
@@ -21,6 +21,9 @@ const PERMISSIONS = {
   // read assets,
   READ_ASSET: 'readAsset',
   WRITE_ASSET: 'writeAsset',
+
+  // deploy hardware
+  DEPLOY_HARDWARE: 'deployHardware',
 };
 
 const PERMISSIONS_DESCRIPTION = {
@@ -29,6 +32,7 @@ const PERMISSIONS_DESCRIPTION = {
   [PERMISSIONS.READ_MODEL]: 'Read model',
   [PERMISSIONS.WRITE_MODEL]: 'Write model',
   [PERMISSIONS.GENERATIVE_AI]: 'Generative AI',
+  [PERMISSIONS.DEPLOY_HARDWARE]: 'Deploy hardware',
 };
 
 // The role here is applied for the resources that the user is not the owner of
@@ -64,6 +68,7 @@ const ROLES = {
       PERMISSIONS.GENERATIVE_AI,
       PERMISSIONS.READ_ASSET,
       PERMISSIONS.WRITE_ASSET,
+      PERMISSIONS.DEPLOY_HARDWARE,
     ],
     ref: 'admin',
     name: 'Admin',
@@ -78,6 +83,11 @@ const ROLES = {
     ref: 'write_asset',
     name: 'Write asset',
   },
+  deploy_hardware: {
+    permissions: [PERMISSIONS.DEPLOY_HARDWARE],
+    ref: 'deploy_hardware',
+    name: 'Deploy hardware',
+  },
 };
 
 const RESOURCES = {
diff --git a/src/config/socket.js b/src/config/socket.js
index 5b9790ad575f2077840a42221fdc3de663adf1f6..0e6f87c839ef65d3bcb08d874d605846e6277e78 100644
--- a/src/config/socket.js
+++ b/src/config/socket.js
@@ -1,22 +1,50 @@
 const { Server } = require('socket.io');
 const logger = require('./logger');
+const ApiError = require('../utils/ApiError');
+const httpStatus = require('http-status');
+const jwt = require('jsonwebtoken');
+const config = require('./config');
+const { jwtVerify } = require('./passport');
+const { tokenTypes } = require('./tokens');
+const permissionService = require('../services/permission.service');
+const { PERMISSIONS } = require('./roles');
 
 let io = null;
 
 const init = (server) => {
   io = new Server(server, {
     cors: {
-      origin: [
-        /localhost:\d+/,
-        /\.digitalauto\.tech$/,
-        /\.digitalauto\.asia$/,
-        /\.digital\.auto$/,
-        'https://digitalauto.netlify.app',
-      ],
+      origin: config.cors.regex,
       credentials: true,
     },
   });
 
+  io.use(function (socket, next) {
+    if (socket.handshake.query && socket.handshake.query.access_token) {
+      jwt.verify(socket.handshake.query.access_token, config.jwt.secret, async function (err, decoded) {
+        if (err) return next(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
+        await jwtVerify(
+          {
+            type: tokenTypes.ACCESS,
+            sub: decoded.sub,
+          },
+          async (error, user) => {
+            if (error || !user) {
+              return next(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
+            }
+            // if (!(await permissionService.hasPermission(user.id, PERMISSIONS.GENERATIVE_AI)))
+            //   return next(new ApiError(httpStatus.UNAUTHORIZED, 'You are not authorized to access this'));
+            // }
+            socket.user = user;
+            next();
+          }
+        );
+      });
+    } else {
+      next(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
+    }
+  });
+
   io.on('connection', () => {
     logger.info('a user connected');
   });
diff --git a/src/controllers/api.controller.js b/src/controllers/api.controller.js
index 8cf7522b27f73c63d05a1bd943235e2cff8a40f1..498268eec70b18249727c43334245cb0cc39f2a7 100644
--- a/src/controllers/api.controller.js
+++ b/src/controllers/api.controller.js
@@ -31,10 +31,22 @@ const deleteApi = catchAsync(async (req, res) => {
   res.status(httpStatus.NO_CONTENT).send();
 });
 
+const listVSSVersions = catchAsync(async (req, res) => {
+  const versions = await apiService.listVSSVersions();
+  res.send(versions);
+});
+
+const getVSSVersion = catchAsync(async (req, res) => {
+  const version = await apiService.getVSSVersion(req.params.name);
+  res.send(version);
+});
+
 module.exports = {
   createApi,
   getApiByModelId,
   getApi,
   updateApi,
   deleteApi,
+  listVSSVersions,
+  getVSSVersion,
 };
diff --git a/src/controllers/asset.controller.js b/src/controllers/asset.controller.js
index 772f97763d955f371c6d8ceae6d9c7e43542bbe0..45bca37b869350bbc664e9d79f69306e5515a4db 100644
--- a/src/controllers/asset.controller.js
+++ b/src/controllers/asset.controller.js
@@ -18,11 +18,13 @@ const getAssets = catchAsync(async (req, res) => {
   const filter = pick(req.query, ['name', 'type']);
   const options = pick(req.query, ['sortBy', 'limit', 'page']);
 
-  const isAdmin = await permissionService.hasPermission(userId, PERMISSIONS.ADMIN);
-  if (!isAdmin) {
-    filter.created_by = userId;
-  }
+  const result = await assetService.queryAssets(filter, options, userId);
+  res.send(result);
+});
 
+const getAllAssets = catchAsync(async (req, res) => {
+  const filter = pick(req.query, ['name', 'type']);
+  const options = pick(req.query, ['sortBy', 'limit', 'page']);
   const result = await assetService.queryAssets(filter, options);
   res.send(result);
 });
@@ -104,4 +106,5 @@ module.exports = {
   generateToken,
   addAuthorizedUser,
   deleteAuthorizedUser,
+  getAllAssets,
 };
diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js
index 27ab779059045a7a4a7e6092a7f6fd131a67c041..eaa9a7a76ea44022e108a095caa7ff85b5e31143 100644
--- a/src/controllers/auth.controller.js
+++ b/src/controllers/auth.controller.js
@@ -1,10 +1,9 @@
 const httpStatus = require('http-status');
 const catchAsync = require('../utils/catchAsync');
-const { authService, userService, tokenService, emailService, fileService } = require('../services');
+const { authService, userService, tokenService, emailService, logService } = require('../services');
 const config = require('../config/config');
 const ApiError = require('../utils/ApiError');
 const logger = require('../config/logger');
-const image = require('../utils/image');
 
 const register = catchAsync(async (req, res) => {
   const user = await userService.createUser({
@@ -12,10 +11,11 @@ const register = catchAsync(async (req, res) => {
     provider: req.body?.provider || 'Email',
   });
   const tokens = await tokenService.generateAuthTokens(user);
-  res.cookie('token', tokens.refresh.token, {
+  res.cookie(config.jwt.cookie.name, tokens.refresh.token, {
     expires: tokens.refresh.expires,
-    ...config.jwt.cookieRefreshOptions,
+    ...config.jwt.cookie.options,
   });
+
   delete tokens.refresh;
   res.status(httpStatus.CREATED).send({ user, tokens });
 });
@@ -24,25 +24,28 @@ const login = catchAsync(async (req, res) => {
   const { email, password } = req.body;
   const user = await authService.loginUserWithEmailAndPassword(email, password);
   const tokens = await tokenService.generateAuthTokens(user);
-  res.cookie('token', tokens.refresh.token, {
+  res.cookie(config.jwt.cookie.name, tokens.refresh.token, {
     expires: tokens.refresh.expires,
-    ...config.jwt.cookieRefreshOptions,
+    ...config.jwt.cookie.options,
   });
   delete tokens.refresh;
   res.send({ user, tokens });
 });
 
 const logout = catchAsync(async (req, res) => {
-  await authService.logout(req.cookies.token);
-  res.clearCookie('token');
+  await authService.logout(req.cookies[config.jwt.cookie.name]);
+  res.clearCookie(config.jwt.cookie.name);
+  res.clearCookie(config.jwt.cookie.name, {
+    ...config.jwt.cookie.options,
+  });
   res.status(httpStatus.NO_CONTENT).send();
 });
 
 const refreshTokens = catchAsync(async (req, res) => {
-  const tokens = await authService.refreshAuth(req.cookies.token);
-  res.cookie('token', tokens.refresh.token, {
+  const tokens = await authService.refreshAuth(req.cookies[config.jwt.cookie.name]);
+  res.cookie(config.jwt.cookie.name, tokens.refresh.token, {
     expires: tokens.refresh.expires,
-    ...config.jwt.cookieRefreshOptions,
+    ...config.jwt.cookie.options,
   });
   delete tokens.refresh;
 
@@ -50,14 +53,75 @@ const refreshTokens = catchAsync(async (req, res) => {
 });
 
 const forgotPassword = catchAsync(async (req, res) => {
+  const returnRawToken = req.query.return_raw_token;
+
   const resetPasswordToken = await tokenService.generateResetPasswordToken(req.body.email);
-  await emailService.sendResetPasswordEmail(req.body.email, resetPasswordToken);
-  res.status(httpStatus.NO_CONTENT).send();
+
+  let domain = undefined;
+  try {
+    const hostname = new URL(req.get('referer')).hostname;
+    if (hostname === 'auth.digital.auto') {
+      domain = hostname;
+    }
+  } catch (error) {}
+
+  if (returnRawToken) {
+    res.status(httpStatus.OK).send({ resetPasswordToken });
+  } else {
+    await emailService.sendResetPasswordEmail(req.body.email, resetPasswordToken, domain);
+    res.status(httpStatus.NO_CONTENT).send();
+  }
+
+  try {
+    await logService.createLog(
+      {
+        name: 'Forgot password',
+        type: 'forgot_password',
+        created_by: req.body.email,
+        description: `User with email ${req.body.email} has triggered forgot password flow`,
+      },
+      {
+        headers: {
+          origin: req.get('origin'),
+          referer: req.get('referer'),
+        },
+      }
+    );
+  } catch (error) {
+    logger.warn(`Failed to create log - forgot password log: ${error}`);
+  }
 });
 
 const resetPassword = catchAsync(async (req, res) => {
-  await authService.resetPassword(req.query.token, req.body.password);
-  res.status(httpStatus.NO_CONTENT).send();
+  let user;
+  try {
+    user = await authService.resetPassword(req.query.token, req.body.password);
+  } catch (error) {
+    logger.info(`Failed to reset password: ${error}`);
+  } finally {
+    res.status(httpStatus.NO_CONTENT).send();
+  }
+
+  try {
+    await logService.createLog(
+      {
+        name: 'Password reset',
+        type: 'password_reset',
+        created_by: user.email || user.id || user._id,
+        description: `User with email ${user.email}, id ${user.id || user._id} has reset their password`,
+        ref_type: 'user',
+        ref_id: user.id || user._id,
+      },
+      {
+        headers: {
+          origin: req.get('origin'),
+          referer: req.get('referer'),
+        },
+      }
+    );
+  } catch (error) {
+    logger.warn(`Failed to create log: ${error}`);
+  }
 });
 
 const sendVerificationEmail = catchAsync(async (req, res) => {
@@ -77,6 +141,7 @@ const githubCallback = catchAsync(async (req, res) => {
     await authService.githubCallback(code, userId);
     res.redirect(`${origin || 'http://127.0.0.1:3000'}/auth/github/success`);
   } catch (error) {
+    logger.error(error);
     res.status(httpStatus.UNAUTHORIZED).send('Unauthorized. Please try again.');
   }
 });
@@ -100,9 +165,9 @@ const sso = catchAsync(async (req, res) => {
   }
 
   const tokens = await tokenService.generateAuthTokens(user);
-  res.cookie('token', tokens.refresh.token, {
+  res.cookie(config.jwt.cookie.name, tokens.refresh.token, {
     expires: tokens.refresh.expires,
-    ...config.jwt.cookieRefreshOptions,
+    ...config.jwt.cookie.options,
   });
   delete tokens.refresh;
 
diff --git a/src/controllers/extendedApi.controller.js b/src/controllers/extendedApi.controller.js
index 90fda16fd8eecaf2b59ae125f33bec2703b19e6f..fefe6a52f867cd047d8e173ec9abe52e76ff13ee 100644
--- a/src/controllers/extendedApi.controller.js
+++ b/src/controllers/extendedApi.controller.js
@@ -1,15 +1,23 @@
 const httpStatus = require('http-status');
 const catchAsync = require('../utils/catchAsync');
-const { extendedApiService } = require('../services');
+const { extendedApiService, permissionService } = require('../services');
 const pick = require('../utils/pick');
 const ApiError = require('../utils/ApiError');
+const { PERMISSIONS } = require('../config/roles');
 
 const createExtendedApi = catchAsync(async (req, res) => {
+  if (!(await permissionService.hasPermission(req.user?.id, PERMISSIONS.WRITE_MODEL, req.body.model))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden. You do not have permission to update extended API for this model');
+  }
   const extendedApi = await extendedApiService.createExtendedApi(req.body);
   res.status(httpStatus.CREATED).send(extendedApi);
 });
 
 const getExtendedApis = catchAsync(async (req, res) => {
+  const { model } = req.query;
+  if (!(await permissionService.canAccessModel(req.user?.id, model))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden');
+  }
   const filter = pick(req.query, ['apiName', 'model', 'skeleton']);
   const options = pick(req.query, ['sortBy', 'limit', 'page']);
   const result = await extendedApiService.queryExtendedApis(filter, options);
@@ -18,6 +26,10 @@ const getExtendedApis = catchAsync(async (req, res) => {
 
 const getExtendedApi = catchAsync(async (req, res) => {
   const extendedApi = await extendedApiService.getExtendedApiById(req.params.id);
+  const { model } = extendedApi;
+  if (!(await permissionService.canAccessModel(req.user?.id, model))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden');
+  }
   if (!extendedApi) {
     throw new ApiError(httpStatus.NOT_FOUND, 'ExtendedApi not found');
   }
@@ -26,6 +38,9 @@ const getExtendedApi = catchAsync(async (req, res) => {
 
 const getExtendedApiByApiNameAndModel = catchAsync(async (req, res) => {
   const { apiName, model } = req.query;
+  if (!(await permissionService.canAccessModel(req.user?.id, model))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden');
+  }
   const extendedApi = await extendedApiService.getExtendedApiByApiNameAndModel(apiName, model);
   if (!extendedApi) {
     throw new ApiError(httpStatus.NOT_FOUND, 'ExtendedApi not found');
@@ -34,12 +49,12 @@ const getExtendedApiByApiNameAndModel = catchAsync(async (req, res) => {
 });
 
 const updateExtendedApi = catchAsync(async (req, res) => {
-  const extendedApi = await extendedApiService.updateExtendedApiById(req.params.id, req.body);
+  const extendedApi = await extendedApiService.updateExtendedApiById(req.params.id, req.body, req.user.id);
   res.send(extendedApi);
 });
 
 const deleteExtendedApi = catchAsync(async (req, res) => {
-  await extendedApiService.deleteExtendedApiById(req.params.id);
+  await extendedApiService.deleteExtendedApiById(req.params.id, req.user.id);
   res.status(httpStatus.NO_CONTENT).send();
 });
 
diff --git a/src/controllers/genai.controller.js b/src/controllers/genai.controller.js
index 45b616f7a1f62ec87bcece3733835a4ebd3b6373..c2c62c71dc322e7bf4d10741b06afc52901c403a 100644
--- a/src/controllers/genai.controller.js
+++ b/src/controllers/genai.controller.js
@@ -10,6 +10,7 @@ const config = require('../config/config');
 const axios = require('axios');
 const etasAuthorizationData = require('../states/etasAuthorization');
 const moment = require('moment');
+const { setupClient } = require('../utils/setupEtasStream');
 
 dotenv.config();
 
@@ -169,6 +170,8 @@ async function BedrockGenCode({ endpointURL, publicKey, secretKey, inputPrompt,
       }
     }
   } catch (error) {
+    console.log('Error in BedrockGenCode');
+    console.log(error);
     try {
       const bedrockResponse = await bedrock.send(new InvokeModelCommand(input));
       const response = JSON.parse(new TextDecoder().decode(bedrockResponse.body));
@@ -176,7 +179,7 @@ async function BedrockGenCode({ endpointURL, publicKey, secretKey, inputPrompt,
         generation = response.completions[0].data.text;
       }
     } catch (err) {
-      throw new Error('Failed to generate response from Bedrock, please check your endpoint URL, region, keys');
+      throw err;
     }
   }
   return generation;
@@ -251,8 +254,20 @@ const getAccessToken = async () => {
   }
 };
 
+const getInstance = (environment = 'prod') => {
+  switch (environment) {
+    case 'prod':
+      return config.etas.instanceEndpoint;
+    case 'dev':
+      return config.etas.developmentEndpoint;
+    default:
+      return config.etas.instanceEndpoint;
+  }
+};
+
 const generateAIContent = async (req, res) => {
   try {
+    const { environment } = req.params;
     const { prompt } = req.body;
     const authorizationData = etasAuthorizationData.getAuthorizationData();
     let token = authorizationData.accessToken;
@@ -265,9 +280,9 @@ const generateAIContent = async (req, res) => {
       });
     }
 
-    const instance = config.etas.instanceEndpoint;
+    const instance = getInstance(environment);
 
-    // console.log('ETAS_INSTANCE_ENDPOINT', instance);
+    setupClient(token);
 
     const response = await axios.post(
       `https://${instance}/r2mm/GENERATE_AI`,
diff --git a/src/controllers/model.controller.js b/src/controllers/model.controller.js
index 978f373e2cb0de3eb80553023a0f4fd7ab667d9a..c0bdf5100d32852270189e3ac60855126486ef81 100644
--- a/src/controllers/model.controller.js
+++ b/src/controllers/model.controller.js
@@ -1,27 +1,91 @@
 const httpStatus = require('http-status');
-const { modelService, apiService, permissionService } = require('../services');
+const { modelService, apiService, permissionService, extendedApiService } = require('../services');
 const catchAsync = require('../utils/catchAsync');
 const pick = require('../utils/pick');
 const ApiError = require('../utils/ApiError');
 const { PERMISSIONS } = require('../config/roles');
+const logger = require('../config/logger');
 
 const createModel = catchAsync(async (req, res) => {
-  const { cvi, custom_apis, ...reqBody } = req.body;
+  let { cvi, custom_apis, extended_apis, api_data_url, ...reqBody } = req.body;
+
+  if (api_data_url) {
+    const result = await modelService.processApiDataUrl(api_data_url);
+    if (result) {
+      extended_apis = result.extended_apis || extended_apis;
+      reqBody.api_version = result.api_version || reqBody.api_version;
+    }
+  }
+
   const model = await modelService.createModel(req.user.id, {
     ...reqBody,
-    ...(reqBody.custom_apis && { custom_apis: JSON.parse(reqBody.custom_apis) }),
-  });
-  await apiService.createApi({
-    model: model._id,
-    cvi: JSON.parse(cvi),
-    created_by: req.user.id,
   });
 
+  try {
+    if (extended_apis) {
+      await Promise.all(
+        extended_apis.map((api) =>
+          extendedApiService.createExtendedApi({
+            model: model._id,
+            apiName: api.apiName,
+            description: api.description,
+            skeleton: api.skeleton,
+            tags: api.tags,
+            type: api.type,
+            datatype: api.datatype,
+            isWishlist: api.isWishlist || false,
+          })
+        )
+      );
+    }
+  } catch (error) {
+    logger.warn(`Error in creating model (creating extended_apis): ${error}`);
+  }
+
+  try {
+    if (custom_apis) {
+      let apis = custom_apis;
+      try {
+        apis = JSON.parse(custom_apis);
+      } catch (error) {
+        // Do nothing
+      }
+
+      if (Array.isArray(apis)) {
+        await Promise.all(
+          apis.map((api) =>
+            extendedApiService.createExtendedApi({
+              model: model._id,
+              apiName: api.name || api.apiName || 'Vehicle',
+              description: api.description || '',
+              skeleton: api.skeleton || '{}',
+              tags: api.tags || [],
+              type: api.type || 'branch',
+              datatype: api.datatype || (api.type !== 'branch' ? 'string' : null),
+              isWishlist: api.isWishlist || false,
+            })
+          )
+        );
+      }
+    }
+  } catch (error) {
+    logger.warn(`Error in creating model (creating extended_apis): ${error}`);
+  }
+
   res.status(httpStatus.CREATED).send(model);
 });
 
 const listModels = catchAsync(async (req, res) => {
-  const filter = pick(req.query, ['name', 'visibility', 'tenant_id', 'vehicle_category', 'main_api', 'id', 'created_by']);
+  const filter = pick(req.query, [
+    'name',
+    'visibility',
+    'state',
+    'tenant_id',
+    'vehicle_category',
+    'main_api',
+    'id',
+    'created_by',
+  ]);
   const options = pick(req.query, ['sortBy', 'limit', 'page', 'fields']);
   const advanced = pick(req.query, ['is_contributor']);
   const models = await modelService.queryModels(filter, options, advanced, req.user?.id);
@@ -50,7 +114,6 @@ const getModel = catchAsync(async (req, res) => {
     finalResult.contributors = contributors;
     finalResult.members = members;
   }
-
   res.send(finalResult);
 });
 
@@ -94,6 +157,14 @@ const deleteAuthorizedUser = catchAsync(async (req, res) => {
   res.status(httpStatus.NO_CONTENT).send();
 });
 
+const getComputedVSSApi = catchAsync(async (req, res) => {
+  if (!(await permissionService.canAccessModel(req.user?.id, req.params.id))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden');
+  }
+  const data = await apiService.computeVSSApi(req.params.id);
+  res.send(data);
+});
+
 module.exports = {
   createModel,
   listModels,
@@ -102,4 +173,5 @@ module.exports = {
   deleteModel,
   addAuthorizedUser,
   deleteAuthorizedUser,
+  getComputedVSSApi,
 };
diff --git a/src/controllers/prototype.controller.js b/src/controllers/prototype.controller.js
index e61540142322f348b04ee248ae245a9cdc224b14..e962df6fd62bb9d7b28282de3c63ddb3e2fa6ab1 100644
--- a/src/controllers/prototype.controller.js
+++ b/src/controllers/prototype.controller.js
@@ -14,6 +14,20 @@ const createPrototype = catchAsync(async (req, res) => {
   res.status(201).send(prototypeId);
 });
 
+const bulkCreatePrototypes = catchAsync(async (req, res) => {
+  const modelIds = new Set(req.body.map((prototype) => prototype.model_id));
+  if (modelIds.size !== 1) {
+    throw new ApiError(httpStatus.BAD_REQUEST, 'All prototypes must belong to the same model');
+  }
+
+  if (!(await permissionService.hasPermission(req.user.id, PERMISSIONS.READ_MODEL, modelIds.values().next().value))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden');
+  }
+
+  const prototypeIds = await prototypeService.bulkCreatePrototypes(req.user.id, req.body);
+  res.status(201).send(prototypeIds);
+});
+
 const listPrototypes = catchAsync(async (req, res) => {
   const readableModelIds = await permissionService.listReadableModelIds(req.user?.id);
 
@@ -65,7 +79,7 @@ const executeCode = catchAsync(async (req, res) => {
 });
 
 const listPopularPrototypes = catchAsync(async (req, res) => {
-  const prototypes = await prototypeService.listPopularPrototypes(req.user.id);
+  const prototypes = await prototypeService.listPopularPrototypes(req.user?.id);
   res.send(prototypes);
 });
 
@@ -78,4 +92,5 @@ module.exports = {
   listRecentPrototypes,
   listPopularPrototypes,
   executeCode,
+  bulkCreatePrototypes,
 };
diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js
index 865dab58f0c289c5e631631ca460c45f33e939b4..9472b385069fa026ca0692435f02441f0a9e220d 100644
--- a/src/controllers/user.controller.js
+++ b/src/controllers/user.controller.js
@@ -14,7 +14,8 @@ const createUser = catchAsync(async (req, res) => {
 const getUsers = catchAsync(async (req, res) => {
   const filter = pick(req.query, ['name', 'role']);
   const options = pick(req.query, ['sortBy', 'limit', 'page']);
-  const advanced = pick(req.query, ['search', 'includeFullDetails']);
+  const advanced = pick(req.query, ['search', 'includeFullDetails', 'id']);
+
   if (advanced.includeFullDetails) {
     // Check if has permission
     if (!(await permissionService.hasPermission(req.user?.id, PERMISSIONS.ADMIN))) {
diff --git a/src/index.js b/src/index.js
index dd72d881158a6d32839455687d6c89894fa8b92b..54664d15eb7690b8596688159e08309f6daca44f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,6 +4,7 @@ const config = require('./config/config');
 const logger = require('./config/logger');
 const initializeRoles = require('./utils/initializeRoles');
 const { init } = require('./config/socket');
+const setupScheduledCheck = require('./scripts/checkVSSUpdate');
 
 let server;
 mongoose.connect(config.mongoose.url, config.mongoose.options).then(() => {
@@ -40,3 +41,5 @@ process.on('SIGTERM', () => {
     server.close();
   }
 });
+
+setupScheduledCheck();
diff --git a/src/models/extendedApi.model.js b/src/models/extendedApi.model.js
index 19da0b11b5966632b0888a443c8d3712c752de99..1ddde6857bd443610df86dd399e0d9cb1c5a6327 100644
--- a/src/models/extendedApi.model.js
+++ b/src/models/extendedApi.model.js
@@ -3,9 +3,15 @@ const { toJSON, paginate } = require('./plugins');
 
 const tagSchema = mongoose.Schema(
   {
-    tagCategoryId: String,
-    tagCategoryName: String,
-    tag: String,
+    title: {
+      type: String,
+      required: true,
+      trim: true,
+    },
+    description: {
+      type: String,
+      trim: true,
+    },
   },
   {
     _id: false,
@@ -26,14 +32,21 @@ const extendedApiSchema = mongoose.Schema(
     skeleton: {
       type: String,
     },
+    unit: {
+      type: String,
+    },
     type: {
       type: String,
     },
-    data_type: {
+    datatype: {
       type: String,
     },
     description: String,
     tags: [tagSchema],
+    isWishlist: {
+      type: Boolean,
+      default: false,
+    },
   },
   {
     timestamps: true,
diff --git a/src/models/model.model.js b/src/models/model.model.js
index a427b7938f9ba1bd65a63174a76dea13e6be5f77..0dc112a07f7f9afd1d3814d88e160256b4347ce1 100644
--- a/src/models/model.model.js
+++ b/src/models/model.model.js
@@ -4,17 +4,14 @@ const { visibilityTypes } = require('../config/enums');
 
 const tagSchema = mongoose.Schema(
   {
-    tag: {
-      type: String,
-      required: true,
-    },
-    tagCategoryId: {
+    title: {
       type: String,
       required: true,
+      trim: true,
     },
-    tagCategoryName: {
+    description: {
       type: String,
-      required: true,
+      trim: true,
     },
   },
   {
@@ -50,6 +47,12 @@ const modelSchema = mongoose.Schema(
       required: true,
       enums: Object.values(visibilityTypes),
     },
+    state: {
+      type: String,
+      default: 'draft',
+      trim: true,
+      maxLength: 255,
+    },
     vehicle_category: {
       type: String,
       required: true,
@@ -73,6 +76,11 @@ const modelSchema = mongoose.Schema(
     extend: {
       type: mongoose.SchemaTypes.Mixed,
     },
+    api_version: {
+      type: String,
+      trim: true,
+      lowercase: true,
+    },
   },
   {
     timestamps: true,
diff --git a/src/models/plugins/toJSON.plugin.js b/src/models/plugins/toJSON.plugin.js
index d511c9db59253303a2f9b73b3986b1ce1f0636a4..97a7df2e74b560a6ade0c28ebea4517f27d5c701 100644
--- a/src/models/plugins/toJSON.plugin.js
+++ b/src/models/plugins/toJSON.plugin.js
@@ -31,7 +31,7 @@ const toJSON = (schema) => {
       ret.id = ret._id.toString();
       delete ret._id;
       delete ret.__v;
-      // ret.created_at = ret.createdAt;
+      ret.created_at = ret.createdAt;
       delete ret.createdAt;
       delete ret.updatedAt;
       if (transform) {
diff --git a/src/models/prototype.model.js b/src/models/prototype.model.js
index 878efebdfe594c5869df15c397bc4cbebba44cc2..e172d3b430b18712e59a0ffc1a45c62570881b06 100644
--- a/src/models/prototype.model.js
+++ b/src/models/prototype.model.js
@@ -32,6 +32,9 @@ const descriptionSchema = mongoose.Schema(
       type: String,
       max: 255,
     },
+    text: {
+      type: String,
+    },
   },
   {
     _id: false,
@@ -51,17 +54,14 @@ const portfolioSchema = mongoose.Schema(
 
 const tagSchema = mongoose.Schema(
   {
-    tag: {
-      type: String,
-      required: true,
-    },
-    tagCategoryId: {
+    title: {
       type: String,
       required: true,
+      trim: true,
     },
-    tagCategoryName: {
+    description: {
       type: String,
-      required: true,
+      trim: true,
     },
   },
   {
@@ -81,116 +81,131 @@ const ratingSchema = mongoose.Schema({
   },
 });
 
-const prototypeSchema = mongoose.Schema({
-  apis: {
-    type: apiSchema,
-    default: {
-      VSC: [],
-      VSS: [],
+const prototypeSchema = mongoose.Schema(
+  {
+    apis: {
+      type: apiSchema,
+      default: {
+        VSC: [],
+        VSS: [],
+      },
     },
-  },
-  code: {
-    type: String,
-    required: true,
-    default:
-      'from sdv_model import Vehicle import plugins from browser import aio vehicle = Vehicle() # write your code here',
-  },
-  extend: {
-    type: mongoose.SchemaTypes.Mixed,
-  },
-  complexity_level: {
-    type: Number,
-    required: true,
-    min: 1,
-    max: 5,
-    default: 3,
-  },
-  customer_journey: {
-    type: String,
-    required: true,
-    default:
-      ' #Step 1 Who: Driver What: Wipers turned on manually Customer TouchPoints: Windshield wiper switch #Step 2 Who: User What: User opens the car door/trunk and the open status of door/trunk is set to true Customer TouchPoints: Door/trunk handle #Step 3 Who: System What: The wiping is immediately turned off by the software and user is notified Customer TouchPoints: Notification on car dashboard and mobile app ',
-  },
-  description: {
-    type: descriptionSchema,
-    default: {
-      problem: '',
-      says_who: '',
-      solution: '',
-      status: '',
+    code: {
+      type: String,
+      default:
+        'from sdv_model import Vehicle import plugins from browser import aio vehicle = Vehicle() # write your code here',
     },
-  },
-  image_file: {
-    type: String,
-  },
-  journey_image_file: {
-    type: String,
-  },
-  analysis_image_file: {
-    type: String,
-  },
-  model_id: {
-    type: mongoose.SchemaTypes.ObjectId,
-    ref: 'Model',
-    required: true,
-  },
-  name: {
-    type: String,
-    required: true,
-    trim: true,
-    maxLength: 255,
-  },
-  portfolio: {
-    type: portfolioSchema,
-    default: {
-      effort_estimation: 0,
-      needs_addressed: 0,
-      relevance: 0,
+    extend: {
+      type: mongoose.SchemaTypes.Mixed,
+    },
+    complexity_level: {
+      type: Number,
+      required: true,
+      min: 1,
+      max: 5,
+      default: 3,
+    },
+    customer_journey: {
+      type: String,
+      required: true,
+      default:
+        ' #Step 1 Who: Driver What: Wipers turned on manually Customer TouchPoints: Windshield wiper switch #Step 2 Who: User What: User opens the car door/trunk and the open status of door/trunk is set to true Customer TouchPoints: Door/trunk handle #Step 3 Who: System What: The wiping is immediately turned off by the software and user is notified Customer TouchPoints: Notification on car dashboard and mobile app ',
+    },
+    description: {
+      type: descriptionSchema,
+      default: {
+        problem: '',
+        says_who: '',
+        solution: '',
+        status: '',
+      },
+    },
+    image_file: {
+      type: String,
+    },
+    journey_image_file: {
+      type: String,
+    },
+    analysis_image_file: {
+      type: String,
+    },
+    model_id: {
+      type: mongoose.SchemaTypes.ObjectId,
+      ref: 'Model',
+      required: true,
+    },
+    name: {
+      type: String,
+      required: true,
+      trim: true,
+      maxLength: 255,
+    },
+    portfolio: {
+      type: portfolioSchema,
+      default: {
+        effort_estimation: 0,
+        needs_addressed: 0,
+        relevance: 0,
+      },
+    },
+    skeleton: {
+      type: String,
+    },
+    state: {
+      type: String,
+      required: true,
+      default: stateTypes.DEVELOPMENT,
+      enums: Object.values(stateTypes),
+    },
+    tags: {
+      type: [tagSchema],
+    },
+    widget_config: {
+      type: String,
+    },
+    last_viewed: {
+      type: Date,
+    },
+    rated_by: {
+      type: Map,
+      of: ratingSchema,
+      default: {},
+    },
+    autorun: {
+      type: Boolean,
+      default: false,
+    },
+    related_ea_components: {
+      type: String,
+    },
+    partner_logo: {
+      type: String,
+    },
+    created_by: {
+      type: mongoose.SchemaTypes.ObjectId,
+      ref: 'User',
+      required: true,
+    },
+    executed_turns: {
+      type: Number,
+      default: 0,
+    },
+    language: {
+      type: String,
+      default: 'python',
+      maxLength: 20,
+    },
+    requirements: {
+      type: String,
+    },
+    flow: {
+      type: mongoose.SchemaTypes.Mixed,
     },
   },
-  skeleton: {
-    type: String,
-  },
-  state: {
-    type: String,
-    required: true,
-    default: stateTypes.DEVELOPMENT,
-    enums: Object.values(stateTypes),
-  },
-  tags: {
-    type: [tagSchema],
-  },
-  widget_config: {
-    type: String,
-  },
-  last_viewed: {
-    type: Date,
-  },
-  rated_by: {
-    type: Map,
-    of: ratingSchema,
-    default: {},
-  },
-  autorun: {
-    type: Boolean,
-    default: false,
-  },
-  related_ea_components: {
-    type: String,
-  },
-  partner_logo: {
-    type: String,
-  },
-  created_by: {
-    type: mongoose.SchemaTypes.ObjectId,
-    ref: 'User',
-    required: true,
-  },
-  executed_turns: {
-    type: Number,
-    default: 0,
-  },
-});
+  {
+    timestamps: true,
+  }
+);
 
 // add plugin that converts mongoose to json
 prototypeSchema.plugin(toJSON);
diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js
index 585ad5e096fa5160944a8cfe15c40446bd9d1526..467c64c16084a315b5889ff8de8563a4bb1830cd 100644
--- a/src/routes/v1/index.js
+++ b/src/routes/v1/index.js
@@ -56,10 +56,10 @@ const defaultRoutes = [
     path: '/plugins',
     route: pluginsRoute,
   },
-  {
-    path: '/tags',
-    route: tagsRoute,
-  },
+  // {
+  //   path: '/tags',
+  //   route: tagsRoute,
+  // },
   {
     path: '/medias',
     route: mediasRoute,
diff --git a/src/routes/v1/tag.route.js b/src/routes/v1/tag.route.js
index ab174899e36d961fa36cbac367b8a398db9f5612..327a1f4dcdbe2194f0937b6d5fe47f2e0e0b7cde 100644
--- a/src/routes/v1/tag.route.js
+++ b/src/routes/v1/tag.route.js
@@ -6,12 +6,12 @@ const validate = require('../../middlewares/validate');
 
 const router = express.Router();
 
-router.route('/').post(validate(tagValidation.createTag), tagController.createTag);
-router
-  .route('/categories')
-  .get(validate(tagValidation.listTagCategories), tagController.listTagCategories)
-  .post(validate(tagValidation.createTagCategory), tagController.createTagCategory);
-router.route('/categories/:id').put(validate(tagValidation.updateTagCategory), tagController.updateTagCategory);
+// router.route('/').post(validate(tagValidation.createTag), tagController.createTag);
+// router
+//   .route('/categories')
+//   .get(validate(tagValidation.listTagCategories), tagController.listTagCategories)
+//   .post(validate(tagValidation.createTagCategory), tagController.createTagCategory);
+// router.route('/categories/:id').put(validate(tagValidation.updateTagCategory), tagController.updateTagCategory);
 
 // router.get('/categories/:tenantId', async (req, res) => {
 //   try {
diff --git a/src/routes/v2/api.route.js b/src/routes/v2/api.route.js
index b9bd30b50f954b26f6175ba7408dfd9affe79736..b0874c84a4a4e36112840c71fb1419545059466b 100644
--- a/src/routes/v2/api.route.js
+++ b/src/routes/v2/api.route.js
@@ -7,6 +7,9 @@ const config = require('../../config/config');
 
 const router = express.Router();
 
+router.route('/vss').get(apiController.listVSSVersions);
+router.route('/vss/:name').get(validate(apiValidation.getVSSVersion), apiController.getVSSVersion);
+
 router.route('/').post(auth(), validate(apiValidation.createApi), apiController.createApi);
 router
   .route('/:id')
diff --git a/src/routes/v2/asset.route.js b/src/routes/v2/asset.route.js
index b6324ef75d46c6d3ac533e139d58c6ff90bcbdb5..0987599c724d4e60fca274b8e98bb4b387eb929a 100644
--- a/src/routes/v2/asset.route.js
+++ b/src/routes/v2/asset.route.js
@@ -13,24 +13,28 @@ router
   .post(auth(), validate(assetValidation.createAsset), assetController.createAsset)
   .get(auth(), validate(assetValidation.getAssets), assetController.getAssets);
 
+router
+  .route('/manage')
+  .get(auth(), checkPermission(PERMISSIONS.ADMIN), validate(assetValidation.getAssets), assetController.getAllAssets);
+
 router
   .route('/:id')
   .get(
     auth(),
-    checkPermission(PERMISSIONS.READ_ASSET, RESOURCES.ASSET),
     validate(assetValidation.getAsset),
+    checkPermission(PERMISSIONS.READ_ASSET, RESOURCES.ASSET),
     assetController.getAsset
   )
   .patch(
     auth(),
-    checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET),
     validate(assetValidation.updateAsset),
+    checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET),
     assetController.updateAsset
   )
   .delete(
     auth(),
-    checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET),
     validate(assetValidation.deleteAsset),
+    checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET),
     assetController.deleteAsset
   );
 
@@ -38,8 +42,8 @@ router
   .route('/:id/generate-token')
   .post(
     auth(),
-    checkPermission(PERMISSIONS.READ_ASSET, RESOURCES.ASSET),
     validate(assetValidation.generateToken),
+    checkPermission(PERMISSIONS.READ_ASSET, RESOURCES.ASSET),
     assetController.generateToken
   );
 
@@ -47,14 +51,14 @@ router
   .route('/:id/permissions')
   .post(
     auth(),
-    checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET),
     validate(assetValidation.addAuthorizedUser),
+    checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET),
     assetController.addAuthorizedUser
   )
   .delete(
     auth(),
-    checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET),
     validate(assetValidation.deleteAuthorizedUser),
+    checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET),
     assetController.deleteAuthorizedUser
   );
 
diff --git a/src/routes/v2/auth.route.js b/src/routes/v2/auth.route.js
index b4a9908b4f205fe7cdd4703ff0f102fa9d8d89b1..e5598ec0f25d76115ecc30bb4885801ac6ec7b1a 100644
--- a/src/routes/v2/auth.route.js
+++ b/src/routes/v2/auth.route.js
@@ -11,8 +11,8 @@ router.get('/github/callback', authController.githubCallback);
 router.post('/sso', validate(authValidation.sso), authController.sso);
 if (!config.strictAuth) {
   router.post('/register', validate(authValidation.register), authController.register);
-  router.post('/login', validate(authValidation.login), authController.login);
 }
+router.post('/login', validate(authValidation.login), authController.login);
 router.post('/logout', authController.logout);
 router.post('/refresh-tokens', authController.refreshTokens);
 router.post('/forgot-password', validate(authValidation.forgotPassword), authController.forgotPassword);
diff --git a/src/routes/v2/email.route.js b/src/routes/v2/email.route.js
index 71a2bd454b160b43e3fe479e976e4ecfc480389f..a0dc24ed54b0cb323b7a3198ec208b4b03ec307d 100644
--- a/src/routes/v2/email.route.js
+++ b/src/routes/v2/email.route.js
@@ -7,21 +7,21 @@ const config = require('../../config/config');
 
 const router = express.Router();
 
-if (config.env === 'development') {
-  router.route('').post(
-    validate(emailValidation.sendEmail),
-    catchAsync(async (req, res) => {
-      const { to, subject, content } = req.body;
-      let html;
-      try {
-        html = decodeURIComponent(content);
-      } catch (error) {
-        html = content;
-      }
-      await emailService.sendEmail(to, subject, html);
-      res.status(200).send();
-    })
-  );
-}
+// if (config.env === 'development') {
+router.route('').post(
+  validate(emailValidation.sendEmail),
+  catchAsync(async (req, res) => {
+    const { to, subject, content } = req.body;
+    let html;
+    try {
+      html = decodeURIComponent(content);
+    } catch (error) {
+      html = content;
+    }
+    await emailService.sendEmail(to, subject, html);
+    res.status(200).send();
+  })
+);
+// }
 
 module.exports = router;
diff --git a/src/routes/v2/genai.route.js b/src/routes/v2/genai.route.js
index d2af9a09bfb9385db632c67e028b2c3da64f3497..991d05c5432a52036802ef898f8a1ec690116120 100644
--- a/src/routes/v2/genai.route.js
+++ b/src/routes/v2/genai.route.js
@@ -33,6 +33,16 @@ router.post(
   auth({
     optional: !config.strictAuth,
   }),
+  genaiPermission,
+  genaiController.generateAIContent
+);
+
+router.post(
+  '/etas/:environment',
+  auth({
+    optional: !config.strictAuth,
+  }),
+  genaiPermission,
   genaiController.generateAIContent
 );
 
diff --git a/src/routes/v2/model.route.js b/src/routes/v2/model.route.js
index e64fcc3329c2a043d20405122649d39b0fd49536..322016f5348e2db77dfbdc0e9008742f6f7060bc 100644
--- a/src/routes/v2/model.route.js
+++ b/src/routes/v2/model.route.js
@@ -42,6 +42,14 @@ router
     modelController.deleteModel
   );
 
+router.route('/:id/api').get(
+  auth({
+    optional: !config.strictAuth,
+  }),
+  validate(modelValidation.getApiByModelId),
+  modelController.getComputedVSSApi
+);
+
 router
   .route('/:id/permissions')
   .post(
diff --git a/src/routes/v2/prototype.route.js b/src/routes/v2/prototype.route.js
index ce01ca60d74ff107193d4a353462368c5f294e1f..f6add8371c43daf814c251d29fac3a4df4e889a3 100644
--- a/src/routes/v2/prototype.route.js
+++ b/src/routes/v2/prototype.route.js
@@ -20,6 +20,10 @@ router
     prototypeController.listPrototypes
   );
 
+router
+  .route('/bulk')
+  .post(auth(), validate(prototypeValidation.bulkCreatePrototypes), prototypeController.bulkCreatePrototypes);
+
 router.route('/recent').get(auth(), prototypeController.listRecentPrototypes);
 router.route('/popular').get(
   auth({
diff --git a/src/scripts/checkVSSUpdate.js b/src/scripts/checkVSSUpdate.js
new file mode 100644
index 0000000000000000000000000000000000000000..b3d8c537f6413f29f08ca4d413772efa96ed3dc7
--- /dev/null
+++ b/src/scripts/checkVSSUpdate.js
@@ -0,0 +1,92 @@
+const axios = require('axios');
+const fs = require('fs');
+const path = require('path');
+const moment = require('moment');
+const _ = require('lodash');
+const logger = require('../config/logger');
+const { fileService } = require('../services');
+
+const setLastCheckTime = () => {
+  fs.writeFileSync(path.join(__dirname, '../../data/clock.txt'), moment().format());
+};
+
+const getLastCheckTime = () => {
+  try {
+    const lastCheckTime = fs.readFileSync(path.join(__dirname, '../../data/clock.txt'), 'utf8');
+    return moment(lastCheckTime);
+  } catch (error) {
+    return moment().subtract(1, 'hour');
+  }
+};
+
+/**
+ *
+ * @param {{name: string; published_at: string; browser_download_url: string}[]} releases
+ */
+const updateVSS = async (releases) => {
+  try {
+    fs.writeFileSync(path.join(__dirname, '../../data/vss.json'), JSON.stringify(releases, null, 2));
+    logger.info('Updated VSS version list');
+    logger.info('Downloading VSS versions data');
+    const promises = releases.map((release) =>
+      fileService.downloadFile(release.browser_download_url, path.join(__dirname, `../../data/${release.name}.json`))
+    );
+    await Promise.all(promises);
+    logger.info('Downloaded VSS versions data');
+  } catch (error) {
+    logger.error(error);
+  }
+};
+
+const checkUpdateVSS = async () => {
+  try {
+    const vssReleases = (await axios.get('https://api.github.com/repos/COVESA/vehicle_signal_specification/releases')).data;
+    setLastCheckTime();
+    const regex = /v(\d+\.\d+)/;
+
+    const filtered = vssReleases
+      ?.filter((release) => {
+        const match = release.name.match(regex);
+        if (match) {
+          return Number(match[1]) >= 3.0;
+        }
+      })
+      ?.map((release) => {
+        return {
+          name: release.name,
+          published_at: release.published_at,
+          browser_download_url: release.assets?.find((asset) => asset.name.endsWith('.json'))?.browser_download_url,
+        };
+      });
+
+    const vssFilePath = path.join(__dirname, '../../data/vss.json');
+
+    try {
+      if (fs.existsSync(vssFilePath) && _.isEqual(filtered, JSON.parse(fs.readFileSync(vssFilePath, 'utf8')))) return;
+    } catch (error) {
+      logger.warn(error);
+    }
+
+    await updateVSS(filtered);
+  } catch (error) {
+    logger.error(error);
+  }
+};
+
+let interval = null;
+
+const setupScheduledCheck = () => {
+  const dataDirExist = fs.existsSync(path.join(__dirname, '../../data'));
+  if (!dataDirExist) {
+    fs.mkdirSync(path.join(__dirname, '../../data'));
+  }
+  const lastCheckTime = getLastCheckTime();
+  if (moment().diff(lastCheckTime, 'seconds') > 120) {
+    checkUpdateVSS();
+  }
+  if (!interval) {
+    interval = setInterval(checkUpdateVSS, 1000 * 60 * 60 * 24);
+  }
+};
+
+module.exports = setupScheduledCheck;
diff --git a/src/scripts/migrate-api.js b/src/scripts/migrate-api.js
new file mode 100644
index 0000000000000000000000000000000000000000..44b868864a051be80abe3e3744bbe1e610089630
--- /dev/null
+++ b/src/scripts/migrate-api.js
@@ -0,0 +1,48 @@
+const Client = require('mongodb').MongoClient;
+
+const connect = async (url) => {
+  const client = new Client(url, { useUnifiedTopology: true });
+  await client.connect();
+  return client.db();
+};
+
+const main = async () => {
+  db = await connect('mongodb://etas-prod-playground-db:27017/playground-be');
+  const models = await db
+    .collection('models')
+    .find({
+      custom_apis: {
+        $exists: true,
+      },
+    })
+    .toArray();
+
+  await db.collection('models').updateMany({ api_version: { $exists: false } }, { $set: { api_version: 'v4.1' } });
+
+  const before = await db.collection('extendedapis').count();
+  console.log('Before:', before);
+  const promises = [];
+
+  models.forEach(async (model) => {
+    const custom_apis = model.custom_apis;
+    custom_apis.forEach(async (custom_api) => {
+      const newData = {
+        apiName: custom_api.name,
+        model: model._id,
+        skeleton: custom_api.skeleton || '{}',
+        tags: custom_api.tags || [],
+        type: custom_api.type || 'branch',
+        datatype: custom_api.datatype || (custom_api.type !== 'branch' ? 'string' : null),
+        description: custom_api.description || '',
+        isWishlist: true
+      };
+      promises.push(db.collection('extendedapis').insertOne(newData));
+    });
+  });
+
+  await Promise.allSettled(promises).catch((err) => console.error(err));
+  const after = await db.collection('extendedapis').count();
+  console.log('after:', after);
+};
+
+main();
diff --git a/src/services/api.service.js b/src/services/api.service.js
index b7da797f53e09b15c84600c73156956f3bef86fb..a71fcbbc8dd382332effee84d5a6a3333c2c17d4 100644
--- a/src/services/api.service.js
+++ b/src/services/api.service.js
@@ -1,7 +1,11 @@
 const httpStatus = require('http-status');
 const { userService } = require('.');
-const { Api } = require('../models');
+const { Api, Model, ExtendedApi } = require('../models');
 const ApiError = require('../utils/ApiError');
+const fs = require('fs');
+const path = require('path');
+const logger = require('../config/logger');
+const { sortObject } = require('../utils/sort');
 
 /**
  *
@@ -78,10 +82,110 @@ const deleteApi = async (apiId, userId) => {
   await api.remove();
 };
 
+const listVSSVersions = async () => {
+  let versions;
+  try {
+    const rawData = fs.readFileSync(path.join(__dirname, '../../data/vss.json'));
+    versions = rawData ? JSON.parse(rawData, 'utf8') : [];
+  } catch (error) {
+    logger.error(error);
+    versions = [];
+  }
+  return versions;
+};
+
+/**
+ *
+ * @param {string} name
+ * @returns {Promise<object>}
+ */
+const getVSSVersion = async (name) => {
+  const filePath = path.join(__dirname, `../../data/${name}.json`);
+  if (!fs.existsSync(filePath)) {
+    throw new ApiError(httpStatus.NOT_FOUND, 'VSS version not found');
+  }
+  const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
+  return data;
+};
+
+/**
+ *
+ * @param {string} modelId
+ */
+const computeVSSApi = async (modelId) => {
+  const model = await Model.findById(modelId);
+  if (!model) {
+    throw new ApiError(httpStatus.NOT_FOUND, 'Model not found');
+  }
+  let ret = null;
+
+  const mainApi = model.main_api || 'Vehicle';
+  const apiVersion = model.api_version;
+  if (!apiVersion) {
+    ret = {
+      [mainApi]: {
+        description: mainApi,
+        type: 'branch',
+        children: {},
+      },
+    };
+  } else {
+    ret = await getVSSVersion(apiVersion);
+  }
+
+  const extendedApis = await ExtendedApi.find({
+    model: modelId,
+    isWishlist: true,
+  });
+  extendedApis.forEach((extendedApi) => {
+    try {
+      const name = extendedApi.apiName.split('.').slice(1).join('.');
+      if (!name) return;
+      ret[mainApi].children[name] = {
+        description: extendedApi.description,
+        type: extendedApi.type || 'branch',
+        id: extendedApi._id,
+        datatype: extendedApi.datatype,
+        name: extendedApi.apiName,
+        isWishlist: extendedApi.isWishlist,
+      };
+      if (extendedApi.unit) {
+        ret[mainApi].children[name].unit = extendedApi.unit;
+      }
+    } catch (error) {
+      logger.warn(`Error while processing extended API ${extendedApi._id} with name ${extendedApi.apiName}: ${error}`);
+    }
+  });
+
+  try {
+    ret[mainApi].children = sortObject(ret[mainApi].children);
+  } catch (error) {
+    logger.warn(`Error while sorting object: ${error}`);
+  }
+
+  // Nest parent/children apis
+  const len = Object.keys(ret[mainApi].children).length;
+  for (let i = len - 1; i >= 0; i--) {
+    const key = Object.keys(ret[mainApi].children)[i];
+    const parent = key.split('.').slice(0, -1).join('.');
+    if (parent && ret[mainApi].children[parent]) {
+      ret[mainApi].children[parent].children = ret[mainApi].children[parent].children || {};
+      const childKey = key.replace(`${parent}.`, '');
+      ret[mainApi].children[parent].children[childKey] = ret[mainApi].children[key];
+      delete ret[mainApi].children[key];
+    }
+  }
+
+  return ret;
+};
+
 module.exports = {
   createApi,
   getApi,
   getApiByModelId,
   updateApi,
   deleteApi,
+  listVSSVersions,
+  getVSSVersion,
+  computeVSSApi,
 };
diff --git a/src/services/asset.service.js b/src/services/asset.service.js
index c74a836a930c7e7e4a29e423d0c154bd6f0935c8..10199d597bd9291d5e2d54c8fa093195119a5480 100644
--- a/src/services/asset.service.js
+++ b/src/services/asset.service.js
@@ -4,6 +4,8 @@ const { Role } = require('../models');
 const Asset = require('../models/asset.model');
 const ApiError = require('../utils/ApiError');
 const httpStatus = require('http-status');
+const logger = require('../config/logger');
+const { isValidObjectId } = require('mongoose');
 
 /**
  *
@@ -21,14 +23,45 @@ const createAsset = (data) => {
  * @param {string} options.sortBy
  * @param {number} options.limit
  * @param {number} options.page
+ * @param {string} [options.userId]
  */
-const queryAssets = (filter, options) => {
+const queryAssets = async (filter, options, userId) => {
   if (filter.name) {
     filter.name = new RegExp(filter.name, 'i');
   }
   if (filter.type) {
     filter.type = new RegExp(filter.type, 'i');
   }
+  if (userId) {
+    let accessibleIds = [];
+    try {
+      const roles = permissionService.getMappedRoles(await permissionService.getUserRoles(userId));
+      roles?.forEach?.((value, key) => {
+        if (!Array.isArray(value)) {
+          logger.error(`Unexpected role value for ${key}: ${value}`);
+        } else if (
+          (value.includes(PERMISSIONS.READ_ASSET) || value.includes(PERMISSIONS.WRITE_ASSET)) &&
+          isValidObjectId(key)
+        ) {
+          accessibleIds.push(key);
+        }
+      });
+    } catch (error) {
+      logger.error(`Error while find accessible assetIds for user ${userId}: ${error}`);
+    }
+
+    filter.$or = [
+      {
+        _id: {
+          $in: accessibleIds,
+        },
+      },
+      {
+        created_by: userId,
+      },
+    ];
+  }
+
   return Asset.paginate(filter, options);
 };
 
diff --git a/src/services/auth.service.js b/src/services/auth.service.js
index 70bfbecf7ea18c4954fd53024aced5b2ede1388c..97875c1d1a2e8df24735b5d2d0319230a6dc00f0 100644
--- a/src/services/auth.service.js
+++ b/src/services/auth.service.js
@@ -52,6 +52,9 @@ const loginUserWithEmailAndPassword = async (email, password) => {
   if (!user || !(await user.isPasswordMatch(password))) {
     throw new ApiError(httpStatus.UNAUTHORIZED, 'Incorrect email or password');
   }
+  if (user.provider_user_id && !user.password) {
+    throw new ApiError(httpStatus.BAD_REQUEST, 'Please login with SSO');
+  }
   return user;
 };
 
@@ -100,17 +103,14 @@ const refreshAuth = async (refreshToken) => {
  * @returns {Promise}
  */
 const resetPassword = async (resetPasswordToken, newPassword) => {
-  try {
-    const resetPasswordTokenDoc = await tokenService.verifyToken(resetPasswordToken, tokenTypes.RESET_PASSWORD);
-    const user = await userService.getUserById(resetPasswordTokenDoc.user);
-    if (!user) {
-      throw new Error();
-    }
-    await userService.updateUserById(user.id, { password: newPassword });
-    await Token.deleteMany({ user: user.id, type: tokenTypes.RESET_PASSWORD });
-  } catch (error) {
-    throw new ApiError(httpStatus.UNAUTHORIZED, 'Password reset failed');
+  const resetPasswordTokenDoc = await tokenService.verifyToken(resetPasswordToken, tokenTypes.RESET_PASSWORD);
+  const user = await userService.getUserById(resetPasswordTokenDoc.user, true);
+  if (!user) {
+    throw new ApiError(httpStatus.BAD_REQUEST, 'Password reset failed');
   }
+  await userService.updateUserById(user.id, { password: newPassword });
+  await Token.deleteMany({ user: user.id, type: tokenTypes.RESET_PASSWORD });
+  return user;
 };
 
 /**
diff --git a/src/services/email.service.js b/src/services/email.service.js
index 18ee8fe9300c031850013f2865693fb2f165463c..9110ccdfdfe3154bbfda978f93e9feb6272741f8 100644
--- a/src/services/email.service.js
+++ b/src/services/email.service.js
@@ -40,10 +40,10 @@ const sendEmail = async (to, subject, html) => {
  * @param {string} token
  * @returns {Promise}
  */
-const sendResetPasswordEmail = async (to, token) => {
+const sendResetPasswordEmail = async (to, token, domain) => {
   const subject = 'Reset password';
   // replace this url with the link to the reset password page of your front-end app
-  const resetPasswordUrl = `${config.client.baseUrl}/reset-password?token=${token}`;
+  const resetPasswordUrl = `${domain || config.client.baseUrl}/reset-password?token=${token}`;
   const html = resetPasswordTemplate(to, resetPasswordUrl);
   await sendEmail(to, subject, html);
 };
diff --git a/src/services/extendedApi.service.js b/src/services/extendedApi.service.js
index 95ea869fc7683a62f791076318f6cc199012da52..264bb5c1f4016eace709720b70670253d2287df5 100644
--- a/src/services/extendedApi.service.js
+++ b/src/services/extendedApi.service.js
@@ -1,6 +1,8 @@
 const httpStatus = require('http-status');
 const { ExtendedApi } = require('../models');
 const ApiError = require('../utils/ApiError');
+const { permissionService } = require('.');
+const { PERMISSIONS } = require('../config/roles');
 
 /**
  * Create a new ExtendedApi
@@ -8,6 +10,10 @@ const ApiError = require('../utils/ApiError');
  * @returns {Promise<ExtendedApi>}
  */
 const createExtendedApi = async (extendedApiBody) => {
+  const existingExtendedApi = await ExtendedApi.findOne({ apiName: extendedApiBody.apiName, model: extendedApiBody.model });
+  if (existingExtendedApi) {
+    throw new ApiError(httpStatus.BAD_REQUEST, 'An extended API with the same name already exists for this model');
+  }
   return ExtendedApi.create(extendedApiBody);
 };
 
@@ -38,13 +44,17 @@ const getExtendedApiById = async (id) => {
  * Update ExtendedApi by id
  * @param {ObjectId} extendedApiId
  * @param {Object} updateBody
+ * @param {string} userId
  * @returns {Promise<ExtendedApi>}
  */
-const updateExtendedApiById = async (extendedApiId, updateBody) => {
+const updateExtendedApiById = async (extendedApiId, updateBody, userId) => {
   const extendedApi = await getExtendedApiById(extendedApiId);
   if (!extendedApi) {
     throw new ApiError(httpStatus.NOT_FOUND, 'ExtendedApi not found');
   }
+  if (!(await permissionService.hasPermission(userId, PERMISSIONS.WRITE_MODEL, extendedApi.model))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden. You do not have permission to update extended API for this model');
+  }
   Object.assign(extendedApi, updateBody);
   await extendedApi.save();
   return extendedApi;
@@ -53,13 +63,17 @@ const updateExtendedApiById = async (extendedApiId, updateBody) => {
 /**
  * Delete ExtendedApi by id
  * @param {ObjectId} extendedApiId
+ * @param {string} userId
  * @returns {Promise<ExtendedApi>}
  */
-const deleteExtendedApiById = async (extendedApiId) => {
+const deleteExtendedApiById = async (extendedApiId, userId) => {
   const extendedApi = await getExtendedApiById(extendedApiId);
   if (!extendedApi) {
     throw new ApiError(httpStatus.NOT_FOUND, 'ExtendedApi not found');
   }
+  if (!(await permissionService.hasPermission(userId, PERMISSIONS.WRITE_MODEL, extendedApi.model))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden. You do not have permission to update extended API for this model');
+  }
   await extendedApi.remove();
   return extendedApi;
 };
diff --git a/src/services/file.service.js b/src/services/file.service.js
index 3c49c71c3a55f21344510c4eac06f2f37e4f4fdb..fdcc13f86a542f027f54176d5aad39181aed8049 100644
--- a/src/services/file.service.js
+++ b/src/services/file.service.js
@@ -3,6 +3,7 @@ const ApiError = require('../utils/ApiError');
 const httpStatus = require('http-status');
 const logger = require('../config/logger');
 const config = require('../config/config');
+const fs = require('fs');
 
 /**
  *
@@ -47,6 +48,16 @@ const getFileFromURL = async (url, encoding = 'File') => {
   }
 };
 
+const downloadFile = async (url, path) => {
+  try {
+    const arrayBuffer = await (await fetch(url)).arrayBuffer();
+    fs.writeFileSync(path, new Uint8Array(arrayBuffer));
+  } catch (error) {
+    logger.error(`Failed to download file from ${url}`);
+    logger.error(error);
+  }
+};
+
 /**
  *
  * @param {File} file1
@@ -57,4 +68,5 @@ const compareImages = async (file1, file2) => {};
 module.exports = {
   upload,
   getFileFromURL,
+  downloadFile,
 };
diff --git a/src/services/index.js b/src/services/index.js
index 9e1107f585164b0c2f6983ff84d2c2d7920fa7bb..84926b5270805dd23c6557c1d909105574c8c075 100644
--- a/src/services/index.js
+++ b/src/services/index.js
@@ -18,3 +18,4 @@ module.exports.searchService = require('./search.service');
 module.exports.certivityService = require('./certivity.service');
 module.exports.assetService = require('./asset.service');
 module.exports.fileService = require('./file.service');
+module.exports.logService = require('./log.service');
diff --git a/src/services/listener.service.js b/src/services/listener.service.js
index ef9d8529a7776bd9cf756916503d4319e6869814..eec861b4a3226c9a59dbb21e1cf27763b2f802a4 100644
--- a/src/services/listener.service.js
+++ b/src/services/listener.service.js
@@ -4,7 +4,11 @@ const findSocketByUser = (userId) => {
   const io = getIO();
   let socket = null;
   io.sockets.sockets.forEach((value) => {
-    if (String(value.request._query.userId) === String(userId)) {
+    const user = value.user;
+    if (!user) {
+      return;
+    }
+    if (String(user._id) === String(userId)) {
       socket = value;
     }
   });
diff --git a/src/services/log.service.js b/src/services/log.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..171ed24ddc2b1ba211202a4f56629f69a8484fb4
--- /dev/null
+++ b/src/services/log.service.js
@@ -0,0 +1,24 @@
+const { logAxios } = require('../config/axios');
+
+/**
+ *
+ * @param {{
+ * name: string;
+ * type: string;
+ * created_by: string;
+ * ref_type?: string;
+ * ref_id?: string;
+ * parent_id?: string
+ * project_id?: string
+ * image?: string
+ * description?: string
+ * }} message
+ * @returns
+ */
+const createLog = async (message, options) => {
+  return (await logAxios.post('/', message, options)).data;
+};
+
+module.exports = {
+  createLog,
+};
diff --git a/src/services/model.service.js b/src/services/model.service.js
index dc3f2cf4d72978d563159f9429427aef57cb2506..7d540f7e2038f04bf60b390dd892e0b917783602 100644
--- a/src/services/model.service.js
+++ b/src/services/model.service.js
@@ -1,10 +1,13 @@
 const httpStatus = require('http-status');
 const { userService } = require('.');
+const prototypeService = require('./prototype.service');
 const permissionService = require('./permission.service');
 const { Model, Role } = require('../models');
 const ApiError = require('../utils/ApiError');
 const { PERMISSIONS } = require('../config/roles');
 const mongoose = require('mongoose');
+const logger = require('../config/logger');
+const _ = require('lodash');
 
 /**
  *
@@ -236,6 +239,7 @@ const deleteModelById = async (id, userId) => {
   }
 
   await model.remove();
+  await prototypeService.deleteMany({ model_id: id });
 };
 
 /**
@@ -319,19 +323,65 @@ const getAccessibleModels = async (userId) => {
 
 /**
  *
- * @param {string} userId
- * @returns {Promise<string[]>}
+ * @param {object} api
+ * @returns {object}
  */
-const listReadableModelIds = async (userId) => {};
-
-module.exports = {
-  createModel,
-  getModels,
-  queryModels,
-  getModelById,
-  updateModelById,
-  deleteModelById,
-  addAuthorizedUser,
-  deleteAuthorizedUser,
-  getAccessibleModels,
+const convertToExtendedApiFormat = (api) => {
+  const { name, ...rest } = api;
+  return {
+    apiName: name,
+    ...rest,
+  };
 };
+
+/**
+ *
+ * @param {string} apiDataUrl
+ * @returns {Promise<{api_version: string; extended_apis: any[]} | undefined>}
+ */
+const processApiDataUrl = async (apiDataUrl) => {
+  try {
+    const response = await fetch(apiDataUrl);
+    const data = await response.json();
+    const wishlist = [];
+
+    const mainApi = Object.keys(data).at(0) || 'Vehicle';
+
+    Object.entries(data[mainApi].children).forEach(([key, value]) => {
+      if (value.isWishlist) {
+        wishlist.push(convertToExtendedApiFormat(data[mainApi].children[key]));
+        delete data[mainApi].children[key];
+      }
+    });
+
+    const result = {};
+    if (wishlist.length > 0) {
+      result.extended_apis = wishlist;
+    }
+
+    const versionList = require('../../data/vss.json');
+    for (const version of versionList) {
+      const file = require(`../../data/${version.name}.json`);
+      const isEqual = _.isEqual(file, data);
+      if (isEqual) {
+        result.api_version = version.name;
+        break;
+      }
+    }
+
+    return result;
+  } catch (error) {
+    logger.warn(`Error in processing api data url: ${error}`);
+  }
+};
+
+module.exports.createModel = createModel;
+module.exports.getModels = getModels;
+module.exports.queryModels = queryModels;
+module.exports.getModelById = getModelById;
+module.exports.updateModelById = updateModelById;
+module.exports.deleteModelById = deleteModelById;
+module.exports.addAuthorizedUser = addAuthorizedUser;
+module.exports.deleteAuthorizedUser = deleteAuthorizedUser;
+module.exports.getAccessibleModels = getAccessibleModels;
+module.exports.processApiDataUrl = processApiDataUrl;
diff --git a/src/services/permission.service.js b/src/services/permission.service.js
index d9e914c3105d402e4648664259f7711c964427af..499246387ebac11fc5b4dd2ce9147f2e30a1a9d1 100644
--- a/src/services/permission.service.js
+++ b/src/services/permission.service.js
@@ -121,12 +121,12 @@ const getMappedRoles = (roles) => {
       if (!Array.isArray(existingRolePermissions)) {
         existingRolePermissions = [];
       } else {
-        const newArray = existingRolePermissions.concat(role.role.permissions);
+        const newArray = existingRolePermissions.concat(role?.role?.permissions || []);
         existingRolePermissions = Array.from(new Set(newArray));
       }
       map.set(roleRef, existingRolePermissions);
     } else {
-      map.set(roleRef, role.role.permissions);
+      map.set(roleRef, role?.role?.permissions || []);
     }
   });
   return map;
@@ -135,7 +135,9 @@ const getMappedRoles = (roles) => {
 // Check if the role map contains the permission
 const containsPermission = (roleMap, permission, id) => {
   const stringId = String(id);
+  // In case user is admin, have access to all type of resources
   const firstCondition = roleMap.has('*') && roleMap.get('*').includes(permission);
+  // In case user has access to specific resource
   const secondCondition = roleMap.has(stringId) && roleMap.get(stringId).includes(permission);
   return firstCondition || secondCondition;
 };
@@ -154,7 +156,7 @@ const checkModelPermission = (model, userId, permission) => {
 };
 
 const checkPrototypePermission = (prototype, userId, permission) => {
-  if (String(prototype.created_by) === String(userId)) {
+  if (String(prototype.created_by) === String(userId) || String(prototype.model_id?.created_by) === String(userId)) {
     return true;
   }
 
@@ -256,6 +258,21 @@ const listReadableModelIds = async (userId) => {
   return Array.from(results);
 };
 
+/**
+ *
+ * @param {string} userId
+ * @param {string} modelId
+ * @returns {Promise<boolean>}
+ */
+const canAccessModel = async (userId, modelId) => {
+  const model = await Model.findById(modelId);
+  if (!model) {
+    throw new ApiError(httpStatus.NOT_FOUND, 'Model not found');
+  }
+  if (model.visibility === 'public') return true;
+  return hasPermission(userId, PERMISSIONS.READ_MODEL, modelId);
+};
+
 module.exports = {
   listAuthorizedUser,
   assignRoleToUser,
@@ -268,4 +285,5 @@ module.exports = {
   getRoles,
   getPermissions,
   listReadableModelIds,
+  canAccessModel,
 };
diff --git a/src/services/prototype.service.js b/src/services/prototype.service.js
index 6014f45c20c1266219a26b48348e6bb4aa2e5bba..d2c331cbfd9bcbceef68d7af3f6df702a1f3ec26 100644
--- a/src/services/prototype.service.js
+++ b/src/services/prototype.service.js
@@ -7,6 +7,7 @@ const { default: axios, isAxiosError } = require('axios');
 const config = require('../config/config');
 const logger = require('../config/logger');
 const modelService = require('./model.service');
+const _ = require('lodash');
 
 /**
  *
@@ -29,6 +30,31 @@ const createPrototype = async (userId, prototypeBody) => {
   return prototype;
 };
 
+/**
+ *
+ * @param {string} userId
+ * @param {Object[]} prototypes
+ * @returns {Promise<string>}create
+ */
+const bulkCreatePrototypes = async (userId, prototypes) => {
+  for (const prototype of prototypes) {
+    if (await Prototype.existsPrototypeInModel(prototype.model_id, prototype.name)) {
+      throw new ApiError(
+        httpStatus.BAD_REQUEST,
+        `Duplicate prototype name '${prototype.name}' in model ${prototype.model_id}`
+      );
+    }
+  }
+
+  const data = await Prototype.insertMany(
+    prototypes.map((prototype) => ({
+      ...prototype,
+      created_by: userId,
+    }))
+  );
+  return data.map((item) => item._id);
+};
+
 /**
  * Query for users
  * @param {Object} filter - Mongo filter
@@ -149,7 +175,8 @@ const listRecentPrototypes = async (userId) => {
 
   const prototypes = await Prototype.find({ _id: { $in: Array.from(prototypeMap.keys()) } })
     .select('name model_id description image_file executed_turns')
-    .populate('model', 'name visibility');
+    .populate('model', 'name visibility')
+    .populate('created_by', 'name image_file');
 
   const results = [];
   recentData.forEach((data) => {
@@ -189,20 +216,33 @@ const listPopularPrototypes = async () => {
   ).map((model) => String(model._id));
   return Prototype.find({
     model_id: { $in: publicModelIds },
+    state: 'Released',
   })
     .sort({ executed_turns: -1 })
     .limit(8)
     .select('name model_id description image_file executed_turns')
-    .populate('model', 'name visibility');
+    .populate('model', 'name visibility')
+    .populate('created_by', 'name image_file');
 };
 
-module.exports = {
-  createPrototype,
-  queryPrototypes,
-  getPrototypeById,
-  updatePrototypeById,
-  deletePrototypeById,
-  listRecentPrototypes,
-  executeCode,
-  listPopularPrototypes,
+/**
+ *
+ * @param {object} filter
+ */
+const deleteMany = async (filter) => {
+  if (_.isEmpty(filter)) {
+    throw new Error('Filter is required');
+  }
+  await Prototype.deleteMany(filter);
 };
+
+module.exports.createPrototype = createPrototype;
+module.exports.queryPrototypes = queryPrototypes;
+module.exports.getPrototypeById = getPrototypeById;
+module.exports.updatePrototypeById = updatePrototypeById;
+module.exports.deletePrototypeById = deletePrototypeById;
+module.exports.listRecentPrototypes = listRecentPrototypes;
+module.exports.executeCode = executeCode;
+module.exports.listPopularPrototypes = listPopularPrototypes;
+module.exports.bulkCreatePrototypes = bulkCreatePrototypes;
+module.exports.deleteMany = deleteMany;
diff --git a/src/services/user.service.js b/src/services/user.service.js
index 4493a5c32080675da40e54e53b0ff81b9da379f4..1ee016f3a6916cc1c343ea9dac02e8f453cc337f 100644
--- a/src/services/user.service.js
+++ b/src/services/user.service.js
@@ -3,6 +3,8 @@ const { User } = require('../models');
 const ApiError = require('../utils/ApiError');
 const image = require('../utils/image');
 const fileService = require('./file.service');
+const logger = require('../config/logger');
+const { isValidObjectId } = require('mongoose');
 
 /**
  * Create a user
@@ -26,9 +28,23 @@ const createUser = async (userBody) => {
  * @param {Object} advanced - Advanced search options
  * @param {string} [advanced.search] - Full text search
  * @param {string} [advanced.includeFullDetails] - Whether to include full user details or not
+ * @param {string} [advanced.id] - Whether to filter users by id
  * @returns {Promise<QueryResult>}
  */
 const queryUsers = async (filter, options, advanced) => {
+  if (advanced.id) {
+    const ids = advanced.id.split(',');
+    for (const id of ids) {
+      if (!isValidObjectId(id)) {
+        throw new ApiError(httpStatus.BAD_REQUEST, `Invalid id ${id}`);
+      }
+    }
+    filter = {
+      ...filter,
+      _id: { $in: ids },
+    };
+  }
+
   if (advanced.search) {
     filter = {
       $and: [
@@ -140,13 +156,18 @@ const updateSSOUser = async (user, graphData) => {
     updateBody.name = graphData.displayName;
   }
 
-  if (userPhoto) {
-    const photoBuffer = await userPhoto.arrayBuffer();
-    const diff = await image.diff(user?.image_file, photoBuffer);
-    if (diff > 0.1 || diff === -1) {
-      const { url } = await fileService.upload(userPhoto);
-      updateBody.image_file = url;
+  try {
+    if (userPhoto) {
+      const photoBuffer = await userPhoto.arrayBuffer();
+      const diff = await image.diff(user?.image_file, photoBuffer);
+      if (diff > 0.1 || diff === -1) {
+        const { url } = await fileService.upload(userPhoto);
+        updateBody.image_file = url;
+      }
     }
+  } catch (error) {
+    logger.error('Error updating user photo');
+    logger.error(error);
   }
 
   if (Object.keys(updateBody).length === 0) {
@@ -170,9 +191,14 @@ const createSSOUser = async (graphData) => {
     provider_user_id: graphData.id,
   };
 
-  if (userPhoto) {
-    const { url } = await fileService.upload(userPhoto);
-    userBody.image_file = url;
+  try {
+    if (userPhoto) {
+      const { url } = await fileService.upload(userPhoto);
+      userBody.image_file = url;
+    }
+  } catch (error) {
+    logger.error('Error creating user photo');
+    logger.error(error);
   }
 
   return createUser(userBody);
diff --git a/src/utils/emailTemplates.js b/src/utils/emailTemplates.js
index e6e1194178d53e9dd1a279cbe8f76b56a1fa21b2..e0973eafed7aa724ced0db3abda31d20fa1f45e6 100644
--- a/src/utils/emailTemplates.js
+++ b/src/utils/emailTemplates.js
@@ -42,9 +42,12 @@ const resetPasswordTemplate = (fullName, link) => `
                 display:block;
               " href="${link}">Reset Password</a>
           </button>
+          <p>
+            or <a href="${link}">click here</a> to reset password if you cannot open the page.
+          </p>
           <p>
             If you did not reset your password, you should visit
-            <a style="text-decoration: none" href="#">your recent accesses</a>
+            <a href="#">your recent accesses</a>
             to this account.
           </p>
           <p class="author" style="font-size:0.875rem;font-weight:600;">digital.auto</p>
diff --git a/src/utils/setupEtasStream.js b/src/utils/setupEtasStream.js
new file mode 100644
index 0000000000000000000000000000000000000000..4e3e1942965098cde9fbfa3480c9ffe52130348c
--- /dev/null
+++ b/src/utils/setupEtasStream.js
@@ -0,0 +1,36 @@
+const WebSocketClient = require('websocket').client;
+const { getIO } = require('../config/socket');
+const config = require('../config/config.js');
+let client = null;
+
+const setupClient = (token) => {
+  if (!client) {
+    const client = new WebSocketClient();
+
+    client.on('connectFailed', function (error) {
+      console.log('Connect etas websocket error: ' + error.toString());
+    });
+    client.on('connect', function (connection) {
+      console.log('Etas WebSocket Client Connected');
+      connection.on('error', function (error) {
+        console.log('Connection Error: ' + error.toString());
+      });
+      connection.on('close', function () {
+        console.log('echo-protocol Connection Closed');
+      });
+      connection.on('message', function (message) {
+        const io = getIO();
+        console.log(message.utf8Data);
+        io?.sockets?.emit('etas-stream', message.utf8Data);
+      });
+    });
+
+    client.connect(`wss://${config.etas.instanceEndpoint}/ws`, null, null, {
+      authorization: `Bearer ${token}`,
+    });
+  }
+};
+
+module.exports = {
+  setupClient,
+};
diff --git a/src/utils/sort.js b/src/utils/sort.js
new file mode 100644
index 0000000000000000000000000000000000000000..a5e37d4873cf595c0c72384e6ea3b5b2aa728454
--- /dev/null
+++ b/src/utils/sort.js
@@ -0,0 +1,12 @@
+const sortObject = (obj) => {
+  return Object.keys(obj)
+    .sort()
+    .reduce((acc, key) => {
+      acc[key] = obj[key];
+      return acc;
+    }, {});
+};
+
+module.exports = {
+  sortObject,
+};
diff --git a/src/validations/api.validation.js b/src/validations/api.validation.js
index 862cd5db6ef865650167f2861c8d98839469c887..be0edfe190ecd27763cd12b567df31170a72f8a8 100644
--- a/src/validations/api.validation.js
+++ b/src/validations/api.validation.js
@@ -38,10 +38,17 @@ const deleteApi = {
   }),
 };
 
+const getVSSVersion = {
+  params: Joi.object().keys({
+    name: Joi.string().required(),
+  }),
+};
+
 module.exports = {
   createApi,
   getApi,
   getApiByModelId,
   updateApi,
   deleteApi,
+  getVSSVersion,
 };
diff --git a/src/validations/auth.validation.js b/src/validations/auth.validation.js
index 92787640bd2c38f8f577194501dcc5867c921ffe..78560c66dc14eb3ac52a6068d641165a7ca9e7c4 100644
--- a/src/validations/auth.validation.js
+++ b/src/validations/auth.validation.js
@@ -25,6 +25,9 @@ const refreshTokens = {
 };
 
 const forgotPassword = {
+  query: Joi.object().keys({
+    return_raw_token: Joi.boolean().default(false),
+  }),
   body: Joi.object().keys({
     email: Joi.string().email().required(),
   }),
diff --git a/src/validations/discussion.validation.js b/src/validations/discussion.validation.js
index 1fc48db1d5844fb3527e89a39e8da046625a1ae7..5315e98262f716c1664910bd7fdbf4ee557af34f 100644
--- a/src/validations/discussion.validation.js
+++ b/src/validations/discussion.validation.js
@@ -1,5 +1,6 @@
 const Joi = require('joi');
 const { objectId } = require('./custom.validation');
+const { populate } = require('../models/token.model');
 
 const createDiscussion = {
   body: Joi.object().keys({
@@ -20,6 +21,7 @@ const listDiscussions = {
     limit: Joi.number().integer(),
     page: Joi.number().integer(),
     fields: Joi.string(),
+    populate: Joi.any(),
   }),
 };
 
diff --git a/src/validations/extendedApi.validation.js b/src/validations/extendedApi.validation.js
index e589b7ed6e2fa51206f911d64f0ce5b508b940cd..8fc0b4437c724b6dbb9b3a687ac34ef5d44ee180 100644
--- a/src/validations/extendedApi.validation.js
+++ b/src/validations/extendedApi.validation.js
@@ -7,24 +7,23 @@ const createExtendedApi = {
     model: Joi.string().custom(objectId).required(),
     skeleton: Joi.string().optional(),
     type: Joi.string(),
-    data_type: Joi.string(),
-    description: Joi.string(),
-    tags: Joi.array()
-      .items(
-        Joi.object({
-          tagCategoryId: Joi.string(),
-          tagCategoryName: Joi.string(),
-          tag: Joi.string(),
-        })
-      )
-      .optional(),
+    datatype: Joi.string(),
+    description: Joi.string().allow('').default(''),
+    tags: Joi.array().items(
+      Joi.object().keys({
+        title: Joi.string().required(),
+        description: Joi.string().allow(''),
+      })
+    ),
+    isWishlist: Joi.boolean().default(false),
+    unit: Joi.string(),
   }),
 };
 
 const getExtendedApis = {
   query: Joi.object().keys({
     apiName: Joi.string(),
-    model: Joi.string().custom(objectId),
+    model: Joi.string().custom(objectId).required(),
     sortBy: Joi.string(),
     limit: Joi.number().integer(),
     page: Joi.number().integer(),
@@ -46,20 +45,17 @@ const updateExtendedApi = {
       apiName: Joi.string()
         .regex(/^Vehicle\./)
         .message('apiName must start with Vehicle.'),
-      model: Joi.string().custom(objectId),
       skeleton: Joi.string().optional(),
       type: Joi.string(),
-      data_type: Joi.string(),
-      description: Joi.string(),
-      tags: Joi.array()
-        .items(
-          Joi.object({
-            tagCategoryId: Joi.string(),
-            tagCategoryName: Joi.string(),
-            tag: Joi.string(),
-          })
-        )
-        .optional(),
+      datatype: Joi.string(),
+      description: Joi.string().allow(''),
+      tags: Joi.array().items(
+        Joi.object().keys({
+          title: Joi.string().required(),
+          description: Joi.string().allow(''),
+        })
+      ),
+      isWishlist: Joi.boolean(),
     })
     .min(1),
 };
diff --git a/src/validations/model.validation.js b/src/validations/model.validation.js
index adac0ac8e5a170228308854145feeb6bb23ea23a..47b1f5beccfe75f008f2e2c75a15ea0f1ca576bc 100644
--- a/src/validations/model.validation.js
+++ b/src/validations/model.validation.js
@@ -6,7 +6,10 @@ const createModel = {
   body: Joi.object().keys({
     extend: Joi.any(),
     custom_apis: Joi.string().custom(jsonString),
-    cvi: Joi.string().required().custom(jsonString),
+    api_version: Joi.string(),
+    api_data_url: Joi.string(),
+    cvi: Joi.string().custom(jsonString),
+    extended_apis: Joi.array().items(Joi.any()),
     main_api: Joi.string().required().max(255),
     model_home_image_file: Joi.string()
       .allow('')
@@ -23,11 +26,11 @@ const createModel = {
     skeleton: Joi.string().custom(jsonString),
     tags: Joi.array().items(
       Joi.object().keys({
-        tag: Joi.string().required(),
-        tagCategoryId: Joi.string().required().custom(slug),
-        tagCategoryName: Joi.string().required(),
+        title: Joi.string().required(),
+        description: Joi.string().allow(''),
       })
     ),
+    state: Joi.string().max(255).default('draft'),
   }),
 };
 
@@ -53,6 +56,7 @@ const updateModel = {
     .keys({
       extend: Joi.any(),
       custom_apis: Joi.string().custom(jsonString),
+      api_version: Joi.string(),
       cvi: Joi.string().custom(jsonString),
       main_api: Joi.string().max(255),
       model_home_image_file: Joi.string().allow(''),
@@ -64,11 +68,11 @@ const updateModel = {
       skeleton: Joi.string().custom(jsonString),
       tags: Joi.array().items(
         Joi.object().keys({
-          tag: Joi.string().required(),
-          tagCategoryId: Joi.string().required().custom(slug),
-          tagCategoryName: Joi.string().required(),
+          title: Joi.string().required(),
+          description: Joi.string().allow(''),
         })
       ),
+      state: Joi.string().max(255),
     })
     .min(1),
   params: Joi.object().keys({
@@ -108,6 +112,12 @@ const deleteAuthorizedUser = {
   }),
 };
 
+const getApiByModelId = {
+  params: Joi.object().keys({
+    id: Joi.string().custom(objectId),
+  }),
+};
+
 module.exports = {
   createModel,
   listModels,
@@ -116,4 +126,5 @@ module.exports = {
   deleteModel,
   addAuthorizedUser,
   deleteAuthorizedUser,
+  getApiByModelId,
 };
diff --git a/src/validations/prototype.validation.js b/src/validations/prototype.validation.js
index e8f5bee45d1f69704cf9395bae9453842d34bfe4..2e83c2c6b67c8649d255055354c0992fe22c33ed 100644
--- a/src/validations/prototype.validation.js
+++ b/src/validations/prototype.validation.js
@@ -2,54 +2,55 @@ const Joi = require('joi');
 const { stateTypes } = require('../config/enums');
 const { objectId, jsonString, slug } = require('./custom.validation');
 
-const createPrototype = {
-  body: Joi.object().keys({
-    extend: Joi.any(),
-    state: Joi.string().allow(...Object.values(stateTypes)),
-    apis: Joi.object().keys({
-      VSC: Joi.array().items(Joi.string()),
-      VSS: Joi.array().items(Joi.string()),
-    }),
-    code: Joi.string().allow(''),
-    complexity_level: Joi.number().min(1).max(5),
-    customer_journey: Joi.string().allow(''),
-    description: Joi.object().keys({
-      problem: Joi.string().allow('').max(4095),
-      says_who: Joi.string().allow('').max(4095),
-      solution: Joi.string().allow('').max(4095),
-      status: Joi.string().allow('').max(255),
-    }),
-    image_file: Joi.string().allow(''),
-    journey_image_file: Joi.string().allow(''),
-    analysis_image_file: Joi.string().allow(''),
-    model_id: Joi.string().required().custom(objectId),
-    name: Joi.string().required().max(255),
-    portfolio: Joi.object().keys({
-      effort_estimation: Joi.number(),
-      needs_addressed: Joi.number(),
-      relevance: Joi.number(),
-    }),
-    skeleton: Joi.string().custom(jsonString),
-    tags: Joi.array().items(
-      Joi.object().keys({
-        tag: Joi.string().required(),
-        tagCategoryId: Joi.string().required().custom(slug),
-        tagCategoryName: Joi.string().required(),
-      })
-    ),
-    widget_config: Joi.string().custom(jsonString),
-    autorun: Joi.boolean(),
-    related_ea_components: Joi.string().allow(''),
-    partner_logo: Joi.string().allow(''),
-    // rated_by: Joi.object().pattern(
-    //   /^[0-9a-fA-F]{24}$/,
-    //   Joi.object()
-    //     .required()
-    //     .keys({
-    //       rating: Joi.number().min(1).max(5),
-    //     })
-    // ),
+const bodyValidation = Joi.object().keys({
+  extend: Joi.any(),
+  flow: Joi.any(),
+  state: Joi.string().allow(...Object.values(stateTypes)),
+  apis: Joi.object().keys({
+    VSC: Joi.array().items(Joi.string()),
+    VSS: Joi.array().items(Joi.string()),
   }),
+  code: Joi.string().allow(''),
+  complexity_level: Joi.number().min(1).max(5),
+  customer_journey: Joi.string().allow(''),
+  description: Joi.object().keys({
+    problem: Joi.string().allow('').max(4095),
+    says_who: Joi.string().allow('').max(4095),
+    solution: Joi.string().allow('').max(4095),
+    status: Joi.string().allow('').max(255),
+    text: Joi.string().allow(''),
+  }),
+  image_file: Joi.string().allow(''),
+  journey_image_file: Joi.string().allow(''),
+  analysis_image_file: Joi.string().allow(''),
+  model_id: Joi.string().required().custom(objectId),
+  name: Joi.string().required().max(255),
+  portfolio: Joi.object().keys({
+    effort_estimation: Joi.number(),
+    needs_addressed: Joi.number(),
+    relevance: Joi.number(),
+  }),
+  skeleton: Joi.string().custom(jsonString),
+  tags: Joi.array().items(
+    Joi.object().keys({
+      title: Joi.string().required(),
+      description: Joi.string().allow(''),
+    })
+  ),
+  widget_config: Joi.string().custom(jsonString),
+  autorun: Joi.boolean(),
+  related_ea_components: Joi.string().allow(''),
+  partner_logo: Joi.string().allow(''),
+  language: Joi.string().default('python'),
+  requirements: Joi.string().allow(''),
+});
+
+const createPrototype = {
+  body: bodyValidation,
+};
+
+const bulkCreatePrototypes = {
+  body: Joi.array().items(bodyValidation).min(1),
 };
 
 const listPrototypes = {
@@ -76,6 +77,7 @@ const getPrototype = {
 
 const updatePrototype = {
   body: Joi.object().keys({
+    flow: Joi.any(),
     extend: Joi.any(),
     state: Joi.string().allow(...Object.values(stateTypes)),
     apis: Joi.object().keys({
@@ -90,6 +92,7 @@ const updatePrototype = {
       says_who: Joi.string().allow('').max(4095),
       solution: Joi.string().allow('').max(4095),
       status: Joi.string().allow('').max(255),
+      text: Joi.string().allow(''),
     }),
     image_file: Joi.string().allow(''),
     journey_image_file: Joi.string().allow(''),
@@ -103,15 +106,16 @@ const updatePrototype = {
     skeleton: Joi.string().custom(jsonString),
     tags: Joi.array().items(
       Joi.object().keys({
-        tag: Joi.string().required(),
-        tagCategoryId: Joi.string().required().custom(slug),
-        tagCategoryName: Joi.string().required(),
+        title: Joi.string().required(),
+        description: Joi.string().allow(''),
       })
     ),
     widget_config: Joi.string().custom(jsonString),
     autorun: Joi.boolean(),
     related_ea_components: Joi.string().allow(''),
     partner_logo: Joi.string().allow(''),
+    requirements: Joi.string().allow(''),
+    language: Joi.string(),
     // rated_by: Joi.object().pattern(
     //   /^[0-9a-fA-F]{24}$/,
     //   Joi.object()
@@ -151,4 +155,5 @@ module.exports = {
   updatePrototype,
   deletePrototype,
   executeCode,
+  bulkCreatePrototypes,
 };
diff --git a/src/validations/user.validation.js b/src/validations/user.validation.js
index 5dbcb0618c55db6640feebf43eec356c80e9266f..dfdd5d57d4ea98b5fcbf0f6f19c63f80d71ab2e8 100644
--- a/src/validations/user.validation.js
+++ b/src/validations/user.validation.js
@@ -44,6 +44,7 @@ const getUsers = {
     limit: Joi.number().integer(),
     page: Joi.number().integer(),
     search: Joi.string(),
+    id: Joi.string(),
     includeFullDetails: Joi.boolean().default(false),
   }),
 };
diff --git a/yarn.lock b/yarn.lock
index 70731548a8ccb3cbde22be8128d9e37b660ce4c8..ac02d3d2e64fe50a078406689cd6f256a5104431 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2245,6 +2245,11 @@
   dependencies:
     "@types/node" "*"
 
+"@types/lodash@^4.17.12":
+  version "4.17.12"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.12.tgz#25d71312bf66512105d71e55d42e22c36bcfc689"
+  integrity sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==
+
 "@types/long@^4.0.0":
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
@@ -3058,7 +3063,7 @@ buffer@^5.2.0, buffer@^5.5.0:
     base64-js "^1.3.1"
     ieee754 "^1.1.13"
 
-bufferutil@^4.0.8:
+bufferutil@^4.0.1, bufferutil@^4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea"
   integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==
@@ -3598,6 +3603,14 @@ culvert@^0.1.2:
   resolved "https://registry.yarnpkg.com/culvert/-/culvert-0.1.2.tgz#9502f5f0154a2d5a22a023e79f71cc936fa6ef6f"
   integrity sha1-lQL18BVKLVoioCPnn3HMk2+m728=
 
+d@1, d@^1.0.1, d@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de"
+  integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==
+  dependencies:
+    es5-ext "^0.10.64"
+    type "^2.7.2"
+
 dashdash@^1.12.0:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -3939,10 +3952,10 @@ engine.io-parser@~5.2.1:
   resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49"
   integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==
 
-engine.io@~6.5.2:
-  version "6.5.5"
-  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.5.tgz#430b80d8840caab91a50e9e23cb551455195fc93"
-  integrity sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==
+engine.io@~6.6.0:
+  version "6.6.1"
+  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.1.tgz#a82b1e5511239a0e95fac14516870ee9138febc8"
+  integrity sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og==
   dependencies:
     "@types/cookie" "^0.4.1"
     "@types/cors" "^2.8.12"
@@ -4000,6 +4013,33 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
+es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.63, es5-ext@^0.10.64, es5-ext@~0.10.14:
+  version "0.10.64"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714"
+  integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==
+  dependencies:
+    es6-iterator "^2.0.3"
+    es6-symbol "^3.1.3"
+    esniff "^2.0.1"
+    next-tick "^1.1.0"
+
+es6-iterator@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+  integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==
+  dependencies:
+    d "1"
+    es5-ext "^0.10.35"
+    es6-symbol "^3.1.1"
+
+es6-symbol@^3.1.1, es6-symbol@^3.1.3:
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c"
+  integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==
+  dependencies:
+    d "^1.0.2"
+    ext "^1.7.0"
+
 escalade@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -4204,6 +4244,16 @@ eslint@^7.0.0:
     text-table "^0.2.0"
     v8-compile-cache "^2.0.3"
 
+esniff@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308"
+  integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==
+  dependencies:
+    d "^1.0.1"
+    es5-ext "^0.10.62"
+    event-emitter "^0.3.5"
+    type "^2.7.2"
+
 espree@^7.3.0, espree@^7.3.1:
   version "7.3.1"
   resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
@@ -4252,6 +4302,14 @@ etag@~1.8.1:
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
   integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
 
+event-emitter@^0.3.5:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+  integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+
 event-target-shim@^5.0.0:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
@@ -4416,6 +4474,13 @@ express@^4.17.1:
     utils-merge "1.0.1"
     vary "~1.1.2"
 
+ext@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f"
+  integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==
+  dependencies:
+    type "^2.7.2"
+
 extend-shallow@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -6605,7 +6670,7 @@ lodash.truncate@^4.4.2:
   resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
   integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
 
-lodash@^4.17.14, lodash@^4.7.0:
+lodash@^4.17.14, lodash@^4.17.21, lodash@^4.7.0:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -7071,6 +7136,11 @@ netmask@^2.0.1:
   resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
   integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
 
+next-tick@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
+  integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
+
 nice-try@^1.0.4:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -8668,16 +8738,16 @@ socket.io-parser@~4.2.4:
     "@socket.io/component-emitter" "~3.1.0"
     debug "~4.3.1"
 
-socket.io@^4.7.5:
-  version "4.7.5"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.5.tgz#56eb2d976aef9d1445f373a62d781a41c7add8f8"
-  integrity sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==
+socket.io@^4.8.0:
+  version "4.8.0"
+  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.0.tgz#33d05ae0915fad1670bd0c4efcc07ccfabebe3b1"
+  integrity sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==
   dependencies:
     accepts "~1.3.4"
     base64id "~2.0.0"
     cors "~2.8.5"
     debug "~4.3.2"
-    engine.io "~6.5.2"
+    engine.io "~6.6.0"
     socket.io-adapter "~2.5.2"
     socket.io-parser "~4.2.4"
 
@@ -9392,6 +9462,11 @@ type-is@^1.6.18, type-is@~1.6.17, type-is@~1.6.18:
     media-typer "0.3.0"
     mime-types "~2.1.24"
 
+type@^2.7.2:
+  version "2.7.3"
+  resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486"
+  integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==
+
 typedarray-to-buffer@^3.1.5:
   version "3.1.5"
   resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
@@ -9499,6 +9574,13 @@ use@^3.1.0:
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
+utf-8-validate@^5.0.2:
+  version "5.0.10"
+  resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2"
+  integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==
+  dependencies:
+    node-gyp-build "^4.3.0"
+
 utf-8-validate@^6.0.4:
   version "6.0.4"
   resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.4.tgz#1305a1bfd94cecb5a866e6fc74fd07f3ed7292e5"
@@ -9649,6 +9731,18 @@ websocket-extensions@>=0.1.1:
   resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
   integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
 
+websocket@^1.0.35:
+  version "1.0.35"
+  resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.35.tgz#374197207d7d4cc4c36cbf8a1bb886ee52a07885"
+  integrity sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==
+  dependencies:
+    bufferutil "^4.0.1"
+    debug "^2.2.0"
+    es5-ext "^0.10.63"
+    typedarray-to-buffer "^3.1.5"
+    utf-8-validate "^5.0.2"
+    yaeti "^0.0.6"
+
 whatwg-encoding@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
@@ -9852,18 +9946,6 @@ xregexp@2.0.0:
   resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
   integrity sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=
 
-xss-clean@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/xss-clean/-/xss-clean-0.1.1.tgz#d3ba684d85ccd52054963d01ad6ab36d662db1a5"
-  integrity sha1-07poTYXM1SBUlj0BrWqzbWYtsaU=
-  dependencies:
-    xss-filters "1.2.6"
-
-xss-filters@1.2.6:
-  version "1.2.6"
-  resolved "https://registry.yarnpkg.com/xss-filters/-/xss-filters-1.2.6.tgz#68b39089cb1dff8b9dbc889484839b2f507f5c55"
-  integrity sha1-aLOQicsd/4udvIiUhIObL1B/XFU=
-
 xtend@^4.0.0:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
@@ -9879,6 +9961,11 @@ y18n@^5.0.5:
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
   integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
 
+yaeti@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
+  integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==
+
 yallist@^2.0.0:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"