Instant.dev

JavaScript + Postgres API Framework with ORM, Migrations and Vectors

README

instant


npm

JavaScript + Postgres API Framework with ORM, Migrations and Vectors


[instant.dev](https://instant.dev) provides a fast, reliable and
battle-tested ORM and migration management system for Postgres 13+ built in
JavaScript. For those familiar with Ruby on Rails, instant.dev adds
functionality similar to ActiveRecord to the Node.js, Deno and Bun ecosystems.
We have been using it since 2016 in production at
Autocode where it has managed over 1 billion records in
a 4TB AWS Aurora Postgres instance.

With instant.dev you can:

- Add the Instant ORM and migrations to
  your existing JavaScript or TypeScript project
- Scaffold new Postgres-backed API projects from scratch using
- Generate new migrations, models and endpoints
- Migrate remote databases and deploy in a single step
- Connect to any PostgreSQL host: AWS RDS, Railway, Vercel Postgres, Neon,
  Supabase
- Instantly deploy to Vercel

Are you interested in connecting? Join us on Discord or follow us on



Features


- [CRUD operations](#crud-operations)
  - Create, Read, Update and Destroy records easily
- [Vector fields](#vector-fields)
  - Build AI-integrated applications and add search by easily creating, updating and
    querying against vectorized representations of your data
- [Query composition](#query-composition)
  - Build complex SELECT and UPDATE queries with many layers of nested joins and
    conditional statements
- [Transactions](#transactions)
  - Ensure data consistency within logical transaction blocks that can be rolled
    back to prevent writing orphaned data
- [Input validation](#input-validation)
  - Synchronously validate object fields to ensure the right data is stored
- [Relationship verification](#relationship-verification)
  - Asynchronously validate relationships between one or more fields and
    external resources before saving
- [Calculated and hidden fields](#calculated-and-hidden-fields)
  - Automatically populate object fields based on existing data
- [Lifecycle callbacks](#lifecycle-callbacks)
  - Execute custom logic beforeSave(), afterSave(), beforeDestroy() and
    afterDestroy() to perform necessary build and teardown steps inside of
    transactions
- [Migrations](#migrations)
  - Manage local database state via the filesystem to make branched git
    development a breeze
- [Seeding](#seeding)
  - Provide custom JSON files so that all developers can share the same test
    data across development, testing and staging environments
- [Code generation](#code-generation)
  - Automatically generate models, migrations and endpoints for your project

Table of Contents


3. [Using the instant CLI](#using-the-instant-cli)
6. Kits
   1. Kit: auth
      1. Kit: auth on Autocode
      2. Kit: auth on Vercel

Installation and Usage


  1. ```shell
  2. npm i instant.dev -g
  3. cd ~/projects/my-awesome-project
  4. instant init
  5. ```

That's it! The command line tool will walk you through the process of
initializing your instant.dev project. It will;

- Automatically detect whether this is a new project or an existing one
- Scaffold a new Vercel or Autocode
  project, if necessary
- Ask for your local database credentials
- Initialize necessary files in the _instant/ directory of your project
- Create an initial migration

To install the basic auth kit which comes with a User and AccessToken
model and associated user registration and login endpoints, use:

  1. ```shell
  2. instant kit auth
  3. ```

You can read more in Kit: auth

Using the instant CLI


You can look up documentation for the instant command line utility at any
point by running instant or instant help. The most commonly used methods
are:

- instant g:model to create a new model
- instant g:relationship to create one-to-one or one-to-many relationships
  between models
- instant g:endpoint to automatically scaffold Vercel or Autocode endpoints,
  if applicable
- instant db:migrate to run migrations
- instant db:rollback to rollback migrations
- instant db:rollbackSync to rollback to last synchronized (filesystem x
  database) migration
- instant db:bootstrap to reset your database, run migrations, and seed data
- instant db:add to add remote databases (AWS RDS, Railway, Vercel Postgres,
  Neon, Supabase)
- instant serve to run your server using Vercel, Autocode or the command
  specified in package.json["scripts"]["start"]
- instant sql is a shortcut to psql into any of your databases
- instant deploy to run outstanding migrations and deploy to Vercel or
  Autocode

Using the Instant ORM


Full documentation for the ORM can be found in the
@instant.dev/orm repository. Here's
a quick overview of using the ORM:

Importing with CommonJS:

  1. ```javascript
  2. const InstantORM = require('@instant.dev/orm');
  3. const Instant = new InstantORM();
  4. ```

Importing with ESM:

  1. ```javascript
  2. import InstantORM from '@instant.dev/orm';
  3. const Instant = new InstantORM();
  4. ```

Using the ORM:

  1. ```javascript
  2. // Connect to your database
  3. // Defaults to using instant/db.json[process.env.NODE_ENV || 'development']
  4. await Instant.connect();

  5. // Get the user model: can also use 'user', 'users' to same effect
  6. const User = Instant.Model('User');

  7. // Create a user
  8. let user = await User.create({username: 'Billy'});

  9. // log user JSON
  10. // {id: 1, username: 'Billy', created_at: '...', updated_at: '...'}
  11. console.log(user.toJSON());

  12. // Create multiple models at once
  13. const UserFactory = Instant.ModelFactory('User');
  14. let createdUsers = await UserFactory.create([
  15.   {username: 'Sharon'},
  16.   {username: 'William'},
  17.   {username: 'Jill'}
  18. ]);

  19. // Retrieves users with username containing the string 'ill'
  20. let users = await User.query()
  21.   .where({username__icontains: 'ill'})
  22.   .orderBy('username', 'ASC')
  23.   .select();

  24. // [{username: 'Billy'}, {username: 'Jill'}, {username: 'William'}]
  25. console.log(users.toJSON());

  26. users[0].set('username', 'Silly Billy');
  27. await users[0].save();

  28. // [{username: 'Silly Billy'}, {username: 'Jill'}, {username: 'William'}]
  29. console.log(users.toJSON());
  30. ```

Feature breakdown


CRUD operations


  1. ```javascript
  2. const User = Instant.Model('User');

  3. /* Create */
  4. let user = await User.create({
  5.   email: 'keith@instant.dev',
  6.   username: 'keith'
  7. });
  8. // Can also use new keyword to create, must save after
  9. user = new User({
  10.   email: 'keith@instant.dev',
  11.   username: 'keith'
  12. });
  13. await user.save();

  14. /* Read */
  15. user = await User.find(1); // uses id
  16. user = await User.findBy('email', 'keith@instant.dev');
  17. user = await User.query()
  18.   .where({email: 'keith@instant.dev'})
  19.   .first();
  20. let users = await User.query()
  21.   .where({email: 'keith@instant.dev'})
  22.   .select();

  23. /* Update */
  24. user.set('username', 'keith_h');
  25. await user.save();

  26. // Update by reading from data
  27. user.read({username: 'keith_h'});
  28. await user.save();

  29. // Update or Create By
  30. user = await User.updateOrCreateBy(
  31.   'username',
  32.   {username: 'keith', email: 'keith+new@instant.dev'}
  33. );

  34. // Update query: this will bypass validations and verifications
  35. users = await User.query()
  36.   .where({username: 'keith_h'})
  37.   .update({username: 'keith'});

  38. /* Destroy */
  39. await user.destroy();
  40. await user.destroyCascade(); // destroy model + children (useful for foreign keys)

  41. /* ModelArray methods */
  42. users.setAll('username', 'instant');
  43. users.readAll({username: 'instant'});
  44. await users.saveAll();
  45. await users.destroyAll();
  46. await users.destroyCascade();
  47. ```

Vector fields


instant.dev comes with built-in support for pgvector and the
vector field type. For full instructions on using vectors please check out the

Set a vector engine via a plugin (OpenAI is the default):

File _instant/plugins/000_set_vector_engine.mjs:

  1. ```javascript
  2. import OpenAI from 'openai';
  3. const openai = new OpenAI({apiKey: process.env.OPENAI_API_KEY});

  4. export const plugin = async (Instant) => {
  5.   Instant.Vectors.setEngine(async (values) => {
  6.     const embedding = await openai.embeddings.create({
  7.       model: 'text-embedding-ada-002',
  8.       input: values
  9.     });
  10.     return embedding.data.map(entry => entry.embedding);
  11.   });
  12. };
  13. ```

Explain how we want to automatically store vector fields:

File: _instant/models/blog_post.mjs

  1. ```javascript
  2. import InstantORM from '@instant.dev/orm';

  3. class BlogPost extends InstantORM.Core.Model {

  4.   static tableName = 'blog_posts';

  5. }

  6. // Stores the `title` and `content` fields together as a vector
  7. // in the `content_embedding` vector field
  8. BlogPost.vectorizes(
  9.   'content_embedding',
  10.   (title, content) => `Title: ${title}\n\nBody: ${content}`
  11. );
  12. // optional, just prevents .toJSON() printing the entire array
  13. BlogPost.hides('content_embedding');

  14. export default BlogPost;
  15. ```

And query our vector fields:

  1. ```javascript
  2. const blogPost = await BlogPost.create({title: `My first post`, content: `some content`});
  3. const vector = blogPost.get('content_embedding'); // length 1,536 array

  4. // Find the top 10 blog posts matching "blog posts about dogs"
  5. // Automatically converts query to a vector
  6. let searchBlogPosts = await BlogPost.query()
  7.   .search('content_embedding', 'blog posts about dogs')
  8.   .limit(10)
  9.   .select();
  10. ```

You can read more on vector queries at

Query composition


  1. ```javascript
  2. const User = Instant.Model('User');

  3. // Basic querying
  4. let users = await User.query()
  5.   .where({id__in: [7, 8, 9]})
  6.   .orderBy('username', 'ASC')
  7.   .limit(2)
  8.   .select();

  9. // Query with OR by sending in a list of where objects
  10. users = await User.query()
  11.   .where( // Can pass in arguments or an array
  12.     {id__in: [7, 8, 9]},
  13.     {username__istartswith: 'Rom'}
  14.   )
  15.   .select();

  16. // evaluate custom values with SQL commands
  17. // in this case, get users where their username matches the first part of their
  18. // email address
  19. users = await User.query()
  20.   .where({
  21.     username: email => `SPLIT_PART(${email}, '@', 1)`
  22.   })
  23.   .select();

  24. // Joins
  25. users = await User.query()
  26.   .join('posts', {title__icontains: 'hello'}) // JOIN ON
  27.   .where({
  28.     username: 'fred',
  29.     posts__like_count__gt: 5 // query joined table
  30.   })
  31.   .select();
  32. users.forEach(user => {
  33.   let posts = user.joined('posts');
  34.   console.log(posts.toJSON()); // log all posts
  35. });

  36. // Deeply-nested joins:
  37. // only get users who have followers that have posts with images from imgur
  38. users = await User.query()
  39.   .join('followers__posts__images')
  40.   .where({followers__posts__images__url__contains: 'imgur.com'})
  41.   .select();
  42. // Access user[0].followers[0].posts[0].images[0] with...
  43. users[0].joined('followers')[0].joined('posts')[0].joined('images')[0];

  44. // Queries are immutable and composable
  45. // Each command creates a new query object from the previous one
  46. let query = User.query();
  47. let query2 = query.where({username__istartswith: 'Rom'});
  48. let query3 = query2.orderBy('username', 'ASC');
  49. let allUsers = await query.select();
  50. let romUsers = await query2.select();
  51. let orderedUsers = await query3.select();

  52. // You can also just query raw SQL!
  53. await Instant.database().query(`SELECT * FROM users`);
  54. ```

Transactions


  1. ```javascript
  2. const User = Instant.Model('User');
  3. const Account = Instant.Model('Account');

  4. const txn = Instant.database().createTransaction();

  5. const user = await User.create({email: 'keith@instant.dev'}, txn);
  6. const account = await Account.create({user_id: user.get('id')}, txn);
  7. await txn.commit(); // commit queries to database
  8. // OR...
  9. await txn.rollback(); // if anything went wrong, rollback nullifies the queries

  10. // Can pass transactions to the following Class methods
  11. await Model.find(id, txn);
  12. await Model.findBy(field, value, txn);
  13. await Model.create(data, txn);
  14. await Model.update(id, data, txn);
  15. await Model.updateOrCreateBy(field, data, txn);
  16. await Model.query().count(txn);
  17. await Model.query().first(txn);
  18. await Model.query().select(txn);
  19. await Model.query().update(fields, txn);
  20. // Instance methods
  21. await model.save(txn);
  22. await model.destroy(txn);
  23. await model.destroyCascade(txn);
  24. // Instance Array methods
  25. await modelArray.saveAll(txn);
  26. await modelArray.destroyAll(txn);
  27. await modelArray.destroyCascade(txn);
  28. ```

Input validation


File: _instant/models/user.mjs

  1. ```javascript
  2. import InstantORM from '@instant.dev/orm';

  3. class User extends InstantORM.Core.Model {

  4.   static tableName = 'users';

  5. }

  6. // Validates email and password before .save()
  7. User.validates(
  8.   'email',
  9.   'must be valid',
  10.   v => v && (v + '').match(/.+@.+\.\w+/i)
  11. );
  12. User.validates(
  13.   'password',
  14.   'must be at least 5 characters in length',
  15.   v => v && v.length >= 5
  16. );

  17. export default User;
  18. ```

Now validations can be used;

  1. ```javascript
  2. const User = Instant.Model('User');

  3. try {
  4.   await User.create({email: 'invalid'});
  5. } catch (e) {
  6.   // Will catch a validation error
  7.   console.log(e.details);
  8.   /*
  9.     {
  10.       "email": ["must be valid"],
  11.       "password": ["must be at least 5 characters in length"]
  12.     }
  13.   */
  14. }
  15. ```

Relationship verification


File: _instant/models/user.mjs

  1. ```javascript
  2. import InstantORM from '@instant.dev/orm';

  3. class User extends InstantORM.Core.Model {

  4.   static tableName = 'users';

  5. }

  6. // Before saving to the database, asynchronously compare fields to each other
  7. User.verifies(
  8.   'phone_number',
  9.   'must correspond to country and be valid',
  10.   async (phone_number, country) => {
  11.     let phoneResult = await someAsyncPhoneValidationAPI(phone_number);
  12.     return (phoneResult.valid === true && phoneResult.country === country);
  13.   }
  14. );

  15. export default User;
  16. ```