diff --git a/src/db/migrations/000_initial.ts b/src/db/migrations/000_initial.ts
index 7b65a68..baf2688 100644
--- a/src/db/migrations/000_initial.ts
+++ b/src/db/migrations/000_initial.ts
@@ -207,9 +207,9 @@ async function createUserSessionsTable(query: { context: any }) {
 async function createUserDevicesTable(query: { context: any }) {
   await query.context.createTable($db.tableName('user_devices'), {
     id: {
-      type: DataTypes.INTEGER,
+      type: DataTypes.UUID,
       primaryKey: true,
-      autoIncrement: true
+      defaultValue: () => uuidv4(),
     },
     userId: {
       type: DataTypes.INTEGER,
diff --git a/src/db/types/UserDevice.ts b/src/db/types/UserDevice.ts
index 678396b..44540c2 100644
--- a/src/db/types/UserDevice.ts
+++ b/src/db/types/UserDevice.ts
@@ -1,11 +1,12 @@
 import { DataTypes, } from 'sequelize';
+import { v4 as uuidv4 } from 'uuid';
 
 function UserDevice(): Record<string, any> {
   return {
     id: {
-      type: DataTypes.INTEGER,
+      type: DataTypes.UUID,
       primaryKey: true,
-      autoIncrement: true
+      defaultValue: () => uuidv4(),
     },
     userId: {
       type: DataTypes.INTEGER,
diff --git a/src/models/User.ts b/src/models/User.ts
index 72f92dd..9bba38f 100644
--- a/src/models/User.ts
+++ b/src/models/User.ts
@@ -4,6 +4,7 @@ import { Op } from 'sequelize';
 import { Optional } from '~/types';
 import Role from './Role';
 import RoleName from './RoleName';
+import UserDevice from './UserDevice';
 
 class User {
   public id: number;
@@ -50,6 +51,16 @@ class User {
     ).map((role) => new Role(role.dataValues));
   }
 
+  public async devices(): Promise<UserDevice[]> {
+    return (
+      await $db.UserDevice().findAll({
+        where: {
+          userId: this.id,
+        },
+      })
+    ).map((device) => new UserDevice(device.dataValues));
+  }
+
   public async setRoles(roles: (Role | RoleName | number | string)[]): Promise<void> {
     const inputRoleIds = new Set(
       roles.filter((role) => {
diff --git a/src/models/UserDevice.ts b/src/models/UserDevice.ts
new file mode 100644
index 0000000..fefd3c5
--- /dev/null
+++ b/src/models/UserDevice.ts
@@ -0,0 +1,48 @@
+import { Optional } from '~/types';
+
+class UserDevice {
+  public id: string;
+  public userId: number;
+  public name: string;
+  public createdAt: Optional<Date>;
+
+  constructor({
+    id,
+    userId,
+    name,
+    createdAt = null,
+  }: {
+    id: string;
+    userId: number;
+    name: string;
+    createdAt?: Optional<Date>;
+  }) {
+    this.id = id;
+    this.name = name;
+    this.userId = userId;
+    this.createdAt = createdAt ? new Date(createdAt) : null;
+  }
+
+  public async destroy() {
+    await $db.UserDevice().destroy({
+      where: {
+        id: this.id,
+      },
+    });
+  }
+
+  public async save() {
+    await $db.UserDevice().update(
+      {
+        name: this.name,
+      },
+      {
+        where: {
+          id: this.id,
+        },
+      }
+    );
+  }
+}
+
+export default UserDevice;
diff --git a/src/models/UserSession.ts b/src/models/UserSession.ts
index 5513bff..5672108 100644
--- a/src/models/UserSession.ts
+++ b/src/models/UserSession.ts
@@ -5,17 +5,20 @@ import { Optional } from '~/types';
 class UserSession {
   public id: string;
   public userId: number;
+  public name?: Optional<string>;
   public expiresAt: Optional<Date>;
   public createdAt: Optional<Date>;
 
   constructor({
     id,
     userId,
+    name,
     expiresAt = null,
     createdAt = null,
   }: any) {
     this.id = id;
     this.userId = userId;
+    this.name = name;
     this.expiresAt = expiresAt;
     this.createdAt = createdAt;
 
diff --git a/src/models/index.ts b/src/models/index.ts
index 363374a..883ff14 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -2,6 +2,7 @@ import GPSPoint from "./GPSPoint";
 import Role from "./Role";
 import RoleName from "./RoleName";
 import User from "./User";
+import UserDevice from "./UserDevice";
 import UserSession from "./UserSession";
 
 export {
@@ -9,5 +10,6 @@ export {
   Role,
   RoleName,
   User,
+  UserDevice,
   UserSession,
 };
diff --git a/src/repos/UserDevices.ts b/src/repos/UserDevices.ts
new file mode 100644
index 0000000..3497950
--- /dev/null
+++ b/src/repos/UserDevices.ts
@@ -0,0 +1,32 @@
+import { Optional } from '~/types';
+import { UserDevice } from '../models';
+
+class UserDevices {
+  public async get(deviceId: string): Promise<Optional<UserDevice>> {
+    let dbDevice = await $db.UserDevice().findByPk(deviceId)
+    if (!dbDevice) {
+      dbDevice = await $db.UserDevice().findOne({
+        where: { name: deviceId }
+      });
+
+      if (!dbDevice) {
+        return null;
+      }
+    }
+
+    return new UserDevice(dbDevice.dataValues);
+  }
+
+  public async create(name: string, args: {
+    userId: number,
+  }): Promise<UserDevice> {
+    const device = await $db.UserDevice().create({
+      userId: args.userId,
+      name: name,
+    });
+
+    return new UserDevice(device.dataValues);
+  }
+}
+
+export default UserDevices;
diff --git a/src/repos/index.ts b/src/repos/index.ts
index 7dced50..36c9875 100644
--- a/src/repos/index.ts
+++ b/src/repos/index.ts
@@ -1,17 +1,20 @@
 import Location from './Location';
 import Users from './Users';
+import UserDevices from './UserDevices';
 import UserRoles from './UserRoles';
 import UserSessions from './UserSessions';
 
 class Repositories {
   public location: Location;
   public users: Users;
+  public userDevices: UserDevices;
   public userRoles: UserRoles;
   public userSessions: UserSessions;
 
   constructor() {
     this.location = new Location();
     this.users = new Users();
+    this.userDevices = new UserDevices();
     this.userRoles = new UserRoles();
     this.userSessions = new UserSessions();
   }
diff --git a/src/routes/Route.ts b/src/routes/Route.ts
index 55c8ffa..81da82d 100644
--- a/src/routes/Route.ts
+++ b/src/routes/Route.ts
@@ -1,4 +1,5 @@
 import { Express, Request, Response } from 'express';
+import { UniqueConstraintError, ValidationError } from 'sequelize';
 
 import {
   BadRequest,
@@ -50,7 +51,14 @@ abstract class Route {
       return;
     }
 
-    if (error instanceof BadRequest) {
+    if (error instanceof UniqueConstraintError) {
+      res.status(400).json({
+        error: `A record with the same [${Object.values(error.fields).join(', ')}] already exists`,
+      });
+      return;
+    }
+
+    if (error instanceof BadRequest || error instanceof ValidationError) {
       res.status(400).json({
         error: error.message,
       });
@@ -66,17 +74,18 @@ abstract class Route {
     handlerName: string,
   ) => {
     logRequest(req);
-    // @ts-expect-error
-    const handler = (this[handlerName]) as ((req: Request, res: Response, auth: AuthInfo) => Promise<void>);
-    const preRequestHandler = (<typeof Route> this.constructor).preRequestHandlers[handlerName];
 
     try {
+      // @ts-expect-error
+      const handler = (this[handlerName]) as ((req: Request, res: Response, auth: AuthInfo) => Promise<void>);
+      const preRequestHandler = (<typeof Route> this.constructor).preRequestHandlers[handlerName];
+
       let authInfo: Optional<AuthInfo>
       if (preRequestHandler) {
         authInfo = await preRequestHandler(req, res);
       }
 
-      handler(req, res, authInfo!);
+      await handler(req, res, authInfo!);
     } catch (error) {
       (<typeof Route> this.constructor).handleError(req, res, error);
     }
diff --git a/src/routes/api/v1/Devices.ts b/src/routes/api/v1/Devices.ts
new file mode 100644
index 0000000..38a60d1
--- /dev/null
+++ b/src/routes/api/v1/Devices.ts
@@ -0,0 +1,47 @@
+import { Optional } from '../../../types';
+import { Request, Response } from 'express';
+
+import ApiV1Route from './Route';
+import { AuthInfo, authenticate } from '../../../auth';
+import { BadRequest } from '../../../errors';
+
+class Devices extends ApiV1Route {
+  constructor() {
+    super('/devices');
+  }
+
+  /**
+   * GET /devices
+   *
+   * It returns a JSON object of devices associated with the authenticated user,
+   * in the format `{ devices: UserDevice[] }`.
+   */
+  @authenticate()
+  get = async (_: Request, res: Response, auth: Optional<AuthInfo>) => {
+    res.json({
+      devices: (await auth!.user.devices()),
+    });
+  }
+
+  /**
+   * POST /devices
+   *
+   * It creates a new device associated with the authenticated user.
+   * It expects a JSON object with the following properties:
+   * - `name` (string): The name of the device.
+   *   It must be unique for the user.
+   */
+  @authenticate()
+  post = async (req: Request, res: Response, auth: Optional<AuthInfo>) => {
+    const { name } = req.body;
+
+    if (!name) {
+      throw new BadRequest('Missing name');
+    }
+
+    const device = await $repos.userDevices.create(name, { userId: auth!.user.id });
+    res.json(device);
+  }
+}
+
+export default Devices;
diff --git a/src/routes/api/v1/DevicesById.ts b/src/routes/api/v1/DevicesById.ts
new file mode 100644
index 0000000..5ecfcfe
--- /dev/null
+++ b/src/routes/api/v1/DevicesById.ts
@@ -0,0 +1,79 @@
+import { Optional } from '../../../types';
+import { Request, Response } from 'express';
+
+import ApiV1Route from './Route';
+import { AuthInfo, authenticate } from '../../../auth';
+import { Forbidden, Unauthorized } from '../../../errors';
+import { RoleName, UserDevice } from '../../../models';
+
+class DevicesById extends ApiV1Route {
+  constructor() {
+    super('/devices/:deviceId');
+  }
+
+  private async authenticatedFetch(auth: AuthInfo, deviceId: string): Promise<UserDevice> {
+    const device = await $repos.userDevices.get(deviceId);
+
+    if (!device) {
+      // Throw a 403 instead of a 404 to avoid leaking information
+      throw new Forbidden('You do not have access to this device');
+    }
+
+    if (device.userId !== auth!.user.id) {
+      try {
+        authenticate([RoleName.Admin]);
+      } catch (e) {
+        throw new Unauthorized('You do not have access to this device');
+      }
+    }
+
+    return device;
+  }
+
+  /**
+   * GET /devices/:deviceId
+   *
+   * It returns a JSON object with the requested device.
+   * Note that the device must be associated with the authenticated user,
+   * unless the user is an admin.
+   */
+  @authenticate()
+  get = async (req: Request, res: Response, auth: Optional<AuthInfo>) => {
+    res.json(await this.authenticatedFetch(auth!, req.params.deviceId));
+  }
+
+  /**
+   * PATCH /devices/:deviceId
+   *
+   * It updates the requested device.
+   * Note that the device must be associated with the authenticated user,
+   * unless the user is an admin.
+   */
+  @authenticate()
+  patch = async (req: Request, res: Response, auth: Optional<AuthInfo>) => {
+    const device = await this.authenticatedFetch(auth!, req.params.deviceId);
+    const { name } = req.body;
+    if (name) {
+      device.name = name;
+    }
+
+    await device.save();
+    res.json(device);
+  }
+
+  /**
+   * DELETE /devices/:deviceId
+   *
+   * It deletes the requested device.
+   * Note that the device must be associated with the authenticated user,
+   * unless the user is an admin.
+   */
+  @authenticate()
+  delete = async (req: Request, res: Response, auth: Optional<AuthInfo>) => {
+    const device = await this.authenticatedFetch(auth!, req.params.deviceId);
+    await device.destroy();
+    res.status(204).send();
+  }
+}
+
+export default DevicesById;
diff --git a/src/routes/api/v1/index.ts b/src/routes/api/v1/index.ts
index 3832ced..e0f345c 100644
--- a/src/routes/api/v1/index.ts
+++ b/src/routes/api/v1/index.ts
@@ -1,4 +1,6 @@
 import Auth from "./Auth";
+import Devices from "./Devices";
+import DevicesById from "./DevicesById";
 import GPSData from "./GPSData";
 import Routes from "../../Routes";
 import UserSelf from "./UserSelf";
@@ -6,6 +8,8 @@ import UserSelf from "./UserSelf";
 class ApiV1Routes extends Routes {
   public routes = [
     new Auth(),
+    new Devices(),
+    new DevicesById(),
     new GPSData(),
     new UserSelf(),
   ];