Source-control 

Each Gadget application's definition can be fully captured in text files which are great for version controlling using Git!

Use Gadget's CLI ggt which allows you to clone your app's files into a local directory and manage any changes between your local directory and the Gadget editor (in real time!).

Using git with Gadget 

Cloning your code to a local directory allows you to use source control tools like Git to track changes to your code and collaborate with others, as well as all of the standard Github ecosystem, like Github Actions for CI and CD.

ggt syncs files to and from a local filesystem, but your Gadget app still runs in the cloud. This means you get all the benefits of your local development setup, like your editor with your plugins and configuration, as well as your command line tools for things like linting and formatting. You also get all the benefits of Gadget's managed development and production environments where you don't need to manage any infrastructure, software versions, containers, etc.

Changes made locally are immediately reflected in Gadget with ggt dev, so you can code as you might normally, and still use all of Gadget's web-based tools for building. The editor, API playground, and your application's API will all live-update to reflect your changes.

Setup source control with ggt 

To start source-controlling your app, you'll need to have a Gadget account, a Gadget app, and a local terminal for running commands.

  1. To get started, run the following command into your local terminal (“test-app” should be replaced with the name of your Gadget app in the command).
terminal
npx ggt@latest dev ~/gadget/test-app --app=test-app

This command will download ggt, Gadget's CLI tool, and begin a sync session that two-way syncs between your local copy and Gadget's cloud infrastructure.

  1. Change directories into your app's directory, and notice all your apps' files:
terminal
cd ~/gadget/test-app
ls
  1. Create a repository on Github, and initialize the git repository locally within your app directory:
terminal
git init
  1. From Github grab the remote url for your Github repository and add it to your Gadget app git repo.
terminal
git remote add origin https://github.com/ehmerasim/your-app.git
  1. Commit all your locally synced Gadget files to the repository.
terminal
git commit -m "adding synced gadget files"
  1. Push your commit to Github.
terminal
git push -u origin main

Voila, you've now successfully pushed your Gadget code to be managed by source control!

Git/Github workflow (PR changes) - example 

  1. Create a new Gadget app and set up your Gadget app on GitHub using the above steps. Head back to Github and ensure your repository contains your pushed Gadget files.

  2. Create and checkout to a new branch within your local directory.

terminal
git checkout -b add-shopify-connection
  1. Head back to Gadget and under the Settings page navigate to the Plugins section and click on the Shopify connection to add it.
navigate to settings and add the Shopify connection to source controlled app
  1. Now follow the steps to add the Shopify connection to your Gadget app. If you need a refresher check out our quickstart here.
connect source controlled Gadget app to Shopify
  1. Once your Gadget app is now successfully connected head back to your local Gadget files and notice the instant changes within the model files added for your Shopify models. Also, head to your settings.gadget.ts file and you'll now notice the meta information representing you've successfully connected to Shopify.
Shopify connection in the meta-data file for settings
settings.gadget.ts
JavaScript
1export const settings: GadgetSettings = {
2 type: "gadget/settings/v1",
3 plugins: {
4 connections: {
5 shopify: {
6 apiVersion: "2024-01",
7 enabledModels: ["shopifyProduct", "shopifyProductImage"],
8 type: "partner",
9 scopes: ["read_products"],
10 },
11 },
12 authentications: {
13 settings: {
14 redirectOnSignIn: "/signed-in",
15 signInPath: "/sign-in",
16 unauthorizedUserRedirect: "signInPath",
17 defaultSignedInRoles: ["signed-in"],
18 },
19 methods: {
20 emailPassword: true,
21 googleOAuth: { scopes: ["email", "profile"], offlineAccess: false },
22 },
23 },
24 },
25};
  1. Now commit and push the new file changes from the addition of the Shopify connection to the branch.
terminal
git commit -m "adds shopify files"
  1. Head back over to GitHub and you'll notice the commit changes.
Commit changes to GitHub in source-controlled app
  1. Switch back to your main branch and head to the settings.gadget.ts file and now you'll notice the Shopify connection is removed.
settings.gadget.ts
JavaScript
1export const settings: GadgetSettings = {
2 type: "gadget/settings/v1",
3 plugins: {
4 authentications: {
5 settings: {
6 redirectOnSignIn: "/signed-in",
7 signInPath: "/sign-in",
8 unauthorizedUserRedirect: "signInPath",
9 defaultSignedInRoles: ["signed-in"],
10 },
11 methods: {
12 emailPassword: true,
13 googleOAuth: { scopes: ["email", "profile"], offlineAccess: false },
14 },
15 },
16 },
17};
  1. Back in Github, create a pull request based on your commit and merge it into the main branch.
Merge committed changes in GitHub PR for source-controlled app

Branching within environments workflow - example 

Continuing from the walkthrough example above, let's take a look at branching through different environments.

When creating multiple branches within different environments always ensure you are synced up to the appropriate environment

  1. Stop ggt running on your current environment by hitting CTRL + C.

  2. Head back to Gadget and create a new development environment.

Create new environment for branching into
  1. Back in your local directory run ggt again but this time pass the environment flag specifying the name of the newly created environment so ggt doesn't automatically sync back up with the previous environment.
terminal
ggt dev --env=testing
  1. And now that my local is set up to my testing environment I can go ahead and create a new branch as I normally would.
terminal
git checkout -b test-branch

Deploying changes to production 

When you're ready to deploy any changes to production:

  1. Double-check your development environment: Before you initiate the deployment, make sure your local filesystem is synchronized with your Gadget development environment..

  2. Pull any changes from merged PR's: After your PR is merged, switch to the main branch in your codebase and pull your changes.

  3. Choosing your deployment method:

  • Gadget interface

  • Terminal (using ggt): Alternatively, you can use ggt to deploy your application. ggt deploy will execute a deploy from whereever you run it. ggt will check if you are in the correct environment and confirm that your local and Gadget filesystems are synchronized. If there are any discrepancies or conflicts, ggt will notify you before proceeding with the deployment.

GGT source-control reference commands. 

terminal
USAGE
ggt [COMMAND]
COMMANDS
sync Clone your local filesystem with your environment's filesystem
status Show the status of your local and environment's filesystem
push Push your local filesystem
pull Pull your environment's filesystem
deploy Deploy your environment to production
list List your available applications
login Log in to your account
logout Log out of your account
whoami Print the currently logged in account
version Print this version of ggt
FLAGS
-h, --help Print how to use the command
-v, --verbose Print more verbose output
--json Print all output as newline-delimited JSON
Run "ggt [COMMAND] -h" for more information about a specific command.
ggt status -h
Show changes made to your local filesystem and
your environment's filesystem.
Changes will be calculated from the last time you ran
"ggt dev", "ggt push", or "ggt pull" in the chosen directory.
ggt push -h
Push changes from your local filesystem to your environment's filesystem.
Changes will be calculated from the last time you ran
"ggt dev", "ggt push", or "ggt pull" on your local filesystem.
USAGE
ggt push
EXAMPLES
$ ggt push
$ ggt push --force
$ ggt push --force --env=staging
$ ggt push --force --env=staging --allow-unknown-directory
FLAGS
-a, --app=<name> The application to push files to
-e, --env=<name> The environment to push files to
--force Discard un-synchronized environment changes
Run "ggt push --help" for more information.
ggt pull -h
Pull changes from your environment's filesystem to your local filesystem.
Changes will be calculated from the last time you ran
"ggt dev", "ggt push", or "ggt pull" on your local filesystem.
USAGE
ggt pull
EXAMPLES
$ ggt pull
$ ggt pull --force
$ ggt pull --force --env=staging
$ ggt pull --force --env=staging --app=example --allow-unknown-directory
FLAGS
-a, --app=<name> The application to pull files from
-e, --env=<name> The environment to pull files from
--force Discard un-synchronized local changes
Run "ggt pull --help" for more information.
ggt deploy
Deploy your development environment to production.
Your local filesystem must be in sync with your development
environment before you can deploy.
Changes will be calculated from the last time you ran
"ggt dev", "ggt push", or "ggt pull" on your local filesystem.
USAGE
ggt deploy
EXAMPLES
$ ggt deploy
$ ggt deploy --from=staging
$ ggt deploy --from=staging --force
$ ggt deploy --from=staging --force --allow-problems
FLAGS
-a, --app=<name> The application to deploy
-e, --from=<env> The environment to deploy from
--force Discard un-synchronized environment changes
Run "ggt deploy --help" for more information.

Working with Gadget metadata files 

How Gadget metadata files work 

When you clone your Gadget app using ggt you'll notice a few new metadata files found in your local directory which contain key reference information about Gadget app's models, settings, and permissions.

  • Each model in your app will have a schema.gadget.ts file describing the database for that model
  • Each app will have a root-level settings.gadget.ts describing the app's settings
  • Each app will have an accessControl/permissions.gadget.ts file describing the app's roles and permissions

These metadata files correspond exactly to Gadget's web interface, and changes made in one will be reflected in the other when using ggt dev.

An example app's filesystem looks like this locally:

text
1api/ // the backend folder powering your app's api
2 models/
3 foo/
4 schema.gadget.ts // the metadata for the foo model
5 actions/
6 create.js
7 update.js
8 delete.js
9 bar/
10 schema.gadget.ts // the metadata for the bar model
11 actions/
12 create.js
13 update.js
14 delete.js
15 actions/
16 globalActionA.js
17 globalActionB.js
18web/ // the frontend folder powering your app's experience
19 pages/
20 index.js
21 about.js
22 components/
23 header.js
24 footer.js
25accessControl/
26 permissions.gadget.ts // the metadata for the app's roles and permissions
27settings.gadget.ts // the metadata for the app's settings

Model storageKeys 

The storageKey property in metadata files identifies the actual data on disk for your model or field in Gadget's underlying database. When you rename a model or a field, Gadget uses the storage key to continue to serve the data from the old name. Without this storageKey, Gadget wouldn't know which field got renamed when, so storageKey is a required property on all models and fields.

Changing the storage key for a model or a field will reset the data for that model or field immediately! It is strongly advised to not edit, rename, or remove any storageKey within the metadata files as doing so can result in your app breaking.

Data stored under a particular storageKey in the Gadget database will remain stored under that key for at least 7 days after it stops being used. If you delete a model (and thus delete it's storageKey), but then revert and re-create the model with the same storageKey, the data from before you deleted the model will still be stored and will be found when you make API calls for the model.

Invalid metadata - risks of manual modification 

It is highly recommended that if you need to make changes to your model schema, connections, or roles and permissions you do not directly do so by editing the metadata file, any changes should be configured within Gadget.

Adding objects and properties directly to the metadata file in most instances will be invalid and your app will not work as expected, in most cases this is due to the unique storageKey's Gadget generates for certain objects.

Removing properties from the file can work at times but is also highly risky and should proceed with caution if doing so.

Fatal errors 

Fatal errors in Gadget refer to critical issues that arise due to incorrect, corrupted, or incompatible modifications in the metadata or folder structure within your local directory, leading to a complete failure of your app. These errors are deemed "fatal" because they prevent the proper functioning of how your Gadget app works. If you run into any fatal error head over to Gadget and within your app you'll notice you won't have access to edit anything and are presented with a list of errors that you must resolve locally in order for your app to continue functioning properly.

model.gadget.ts 

JavaScript
1import type { GadgetModel } from "gadget-server";
2
3export const schema: GadgetModel = {
4 type: "gadget/model-schema/v1",
5 storageKey: "DataModel-bbbb123qwerty",
6 fields: {
7 fieldTwo: {
8 type: "string",
9 storageKey: "bbbb",
10 },
11 fieldThree: {
12 type: "richText",
13 storageKey: "bbbb",
14 },
15 fieldFour: {
16 type: "boolean",
17 default: true,
18 storageKey: "bbbb",
19 },
20 fieldFive: {
21 type: "enum",
22 options: ["yo", "dawg"],
23 acceptMultipleSelections: true,
24 acceptUnlistedOptions: true,
25 storageKey: "bbbb",
26 },
27 fieldSix: {
28 type: "dateTime",
29 includeTime: true,
30 default: "2023-10-15T23:19:55Z",
31 storageKey: "bbbb",
32 },
33 fieldSeven: {
34 type: "file",
35 allowPublicAccess: true,
36 storageKey: "bbbb",
37 },
38 fieldEight: {
39 type: "email",
40 storageKey: "bbbb",
41 validations: {
42 email: true,
43 },
44 },
45 fieldNine: {
46 type: "json",
47 default: [
48 {
49 title: "The Great Gatsby",
50 author: "F. Scott Fitzgerald",
51 year: 1925,
52 },
53 ],
54 storageKey: "bbbb",
55 },
56 fieldTen: {
57 type: "computed",
58 sourceFile: "models/modelA/file.gelly",
59 storageKey: "bbbb",
60 },
61 fieldEleven: {
62 type: "belongsTo",
63 parent: {
64 model: "foo",
65 },
66 storageKey: "bbbb",
67 },
68
69 fieldTwelve: {
70 type: "hasMany",
71 child: {
72 model: "foo",
73 belongsToField: "bar",
74 },
75 storageKey: "bbbb",
76 },
77
78 fieldThirteen: {
79 type: "hasManyThrough",
80 sibling: {
81 model: "student",
82 relatedField: "course",
83 },
84 join: {
85 model: "registration",
86 belongsToSelfField: "student",
87 belongsToSiblingField: "course",
88 },
89 storageKey: "bbbb",
90 },
91 fieldFifteen: {
92 type: "roleList",
93 default: ["signedIn"],
94 storageKey: "bbbb",
95 },
96 fieldSixteen: {
97 type: "password",
98 validations: {
99 strongPassword: true,
100 },
101 storageKey: "bbbb",
102 },
103 },
104};

settings.gadget.ts 

JavaScript
1import type { GadgetSettings } from "gadget-server";
2
3export const settings: GadgetSettings = {
4 type: "gadget/settings/v1",
5 frameworkVersion: "0.2",
6 plugins: {
7 connections: {
8 sentry: true,
9 openai: true,
10 shopify: {
11 type: "admin",
12 apiVersion: "2023-07",
13 },
14 shopify: {
15 type: "partner",
16 scopes: ["read_products", "write_products"],
17 apiVersion: "2023-07",
18 },
19 },
20 authentications: {
21 settings: {
22 signInPath: "/sign-in",
23 redirectOnSignIn: "/signed-in",
24 unauthorizedUserRedirect: "signInPath",
25 defaultSignedInRoles: ["signed-in"],
26 },
27 emailPassword: true,
28 googleOAuth: {
29 scopes: [
30 "https://www.googleapis.com/auth/userinfo.profile",
31 "https://www.googleapis.com/auth/userinfo.email",
32 "openid",
33 ],
34 offlineAccess: true,
35 },
36 },
37 },
38};

permissions.gadget.ts 

JavaScript
1import type { GadgetAccessControl } from "gadget-server";
2
3// This file describes your access control permissions
4// For more information on this file http://docs.gadget.dev
5export const accessControl: GadgetAccessControl = {
6 type: "gadget/access-control/v1",
7 roles: {
8 unauthenticated: {
9 storageKey: "unauthenticated",
10 },
11 "custom role": {
12 storageKey: "Role-abc123",
13 default: {
14 read: true,
15 action: true,
16 },
17 models: {
18 modelA: {
19 read: { filter: "models/foo/tenancy.gelly" },
20 actions: {
21 create: true,
22 update: true,
23 delete: true,
24 },
25 },
26 },
27 actions: {
28 globalActionA: true,
29 },
30 },
31 },
32};