Skip to content

Commit

Permalink
Add documentation for secrets (#138)
Browse files Browse the repository at this point in the history
Also some reformatting/editing of the authoring guide.
  • Loading branch information
rohansingh authored Jan 31, 2022
1 parent 0b7a322 commit 367b79e
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 11 deletions.
74 changes: 64 additions & 10 deletions docs/authoring_apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,38 @@
If you haven't already, check out the [tutorial](tutorial.md) for how to get started writing apps. This guide picks up where the tutorial leaves off and provides practices and philosophy on how to build apps using pixlet.

## Architecture
The way pixlet works is heavily influenced by the way Tidbyt works. The Tidbyt displays really tiny 64x32 images and keeps a local cache of images for the installations on the device. On a regular interval, the Tidbyt sends heartbeats to the Tidbyt backend letting the backend know what installations it has inside its cache. The backend then determines what apps need rendered, executes the appropriate Starlark script, and encodes the result as a [WebP](https://developers.google.com/speed/webp) image to send back to the device.
Pixlet is heavily influenced by the way Tidbyt devices work. The Tidbyt displays tiny 64x32 animations and images. It keeps a local cache of the apps that are installed on it.

Each Tidbyt regularly sends heartbeats to the Tidbyt cloud, announcing what it has cached. The Tidbyt cloud decides which app needs to be rendered next, and executes the appropriate Starlark script. It encodes the result as a [WebP](https://developers.google.com/speed/webp) image, and sends it back to the device.

To mimic how we host apps internally, `pixlet render` executes the Starlark script and `pixlet push` pushes the resulting WebP to your Tidbyt.

## Cache
As part of pixlet, we offer the `cache` module to cache results from API requests or other data that's needed between renders. Inside of `pixlet`, it's not super useful given the `pixlet` binary has a short lived execution. When publishing apps using the Tidbyt [Community](https://github.com/tidbyt/community) repo, it can cut down on API requests and add some reliability to your app.
## Config
When running an app, Pixlet passes a `config` object to the app's `main()`:

```starlark
def main(config):
who = config.get("who")
print("Hello, %s" % who)
```

The main thing to keep in mind when working with `cache` is that it's scoped per app. This means that two installations by two different users will have the same cache. Anything that's cached should not be specific to an installation and instead specific to the parameters being requested. Make sure you create a cache key that is unique for the type of information you are caching.
The `config` object contains values that are useful for your app. You can set the actual values by:

## Config
Config is used to configure an app. It's a key/value pair of config values and is passed into `main`. It's exposed through query parameters in the url of `pixlet serve` or through command line args through `pixlet render`. When publishing apps to the Tidbyt [Community](https://github.com/tidbyt/community) repo, the Tidbyt backend will populate the config values from values provided in the mobile app.
1. Passing URL query parameters when using `pixlet serve`.
2. Setting command-line arguments via `pixlet render`.

The important thing to remember here is that your app should always be able to render even if a config value isn't provided. Providing default values for every config value or checking it for `None` will ensure the app behaves as expected even if config was not provided. For example, the following ensures there will always be a value for `foo`:
When apps that are published to the [Tidbyt Community repo][3], users can install and configure them with the Tidbyt smartphone app. [Define a schema for your app][4] to enable this.

Your app should always be able to render, even if a config value isn't provided. Provide defaults for every config value, or check if the value is `None`. This will ensure the app behaves as expected even if config was not provided.

For example, the following ensures there will always be a value for `who`:

```starlark
DEFAULT_FOO = "bar"
DEFAULT_WHO = "world"

def main(config):
foo = config.get("foo", DEFAULT_FOO)
who = config.get("who") or DEFAULT_WHO
print("Hello, %s" % who)
```

The `config` object also has helpers to convert config values into specific types:
Expand All @@ -30,6 +43,47 @@ config.str("foo") # returns a string, or None if not found
config.bool("foo") # returns a boolean (True or False), or None if not found
```

## Cache
Use the `cache` module to cache results from API requests or other data that's needed between renders. We require sensible caching for apps in the [Tidbyt Community repo](https://github.com/tidbyt/community). Caching cuts down on API requests, and can make your app more reliable.

**Make sure to create cache keys that are unique for the type of information you are caching.** Each app has its own cache, but that cache is shared among every user. Two installations of the same app by two different users will share the same cache.

A good strategy is to create cache keys based on the config parameters or information being requested.

## Secrets

Many apps need secret values like API keys. When publishing your app to the [Tidbyt community repo][3], encrypt sensitive values so that only the Tidbyt cloud servers can decrypt them.

To encrypt values, use the `pixlet encrypt` command. For exmaple:

```
$ pixlet encrypt my-app-name top_secret_api_key_123456
"AV6+...." # encrypted value
```

Use the `secret.decrypt()` function in your app to decrypt this value:

```starlark
load("secret.star", "secret")

def main(config):
api_key = secret.decrypt("AV6+...") or config.get("dev_api_key")
```

When you run `pixlet` locally, `secret.decrypt` will always return `None`. When your app runs in the Tidbyt cloud, `secret.decrypt` will return the string that you passed to `pixlet encrypt`.


## Fail
Using a `fail()` inside of your app should be used incredibly sparingly. It kills the entire execution in an unrecoverable fashion. If your app depends on an external API, it cannot cache the response, and has no means of recovering with a failed response - then a `fail()` is appropriate. Otherwise, using a `print()` and handling the error appropriately is a better approach.
The [`fail()`][1] function will immediately end the execution of your app and return an error. It should be used incredibly sparingly, and only in cases that are _permanent_ failures.

For example, if your app receives an error from an external API, try these options before `fail()`:

1. Return a cached response.
2. Display a useful message or fallback data.
3. [`print()`][2] an error message.
3. Handle the error in a way that makes sense for your app.

[1]: https://github.com/bazelbuild/starlark/blob/master/spec.md#fail
[2]: https://github.com/bazelbuild/starlark/blob/master/spec.md#print
[3]: https://github.com/tidbyt/community
[4]: schema.md
20 changes: 19 additions & 1 deletion docs/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ this example, the `render` module is made available to the script as
the symbol `r` instead of `render`:

```starlark
load("render.star", r="render")
load("render.star", r = "render")
```

## Starlib modules
Expand Down Expand Up @@ -117,6 +117,24 @@ Example:

See [examples/schema_hello_world.star](../examples/schema_hello_world.star) for an example.

## Pixlet module: Secret

The secret module can decrypt values that were encrypted with `pixlet encrypt`.

| Function | Description |
| --- | --- |
| `decrypt(value)` | Decrypts and returns the value when running in Tidbyt cloud. Returns `None` when running locally. Decryption will fail if the name of the app doesn't match the name that was passed to `pixlet encrypt`. |

Example:
```starlark
load("secret.star", "secret")

ENCRYPTED_API_KEY = "AV6+..." . # from `pixlet encyrpt`

def main(config):
api_key = secret.decrypt(ENCRYPTED_API_KEY) or config.get("dev_api_key")
```

## Pixlet module: Sunrise

The `sunrise` module calculates sunrise and sunset times for a given set of GPS coordinates and timestamp.
Expand Down

0 comments on commit 367b79e

Please sign in to comment.