Welcome to the SQRL tutorial! Before we begin, let’s go over a few concepts.
SQRL takes an event, called an action, and turns it into one or more features (some special features are also called rules, but we’ll get to that later). Your server-side application will usually send an action to a SQRL service, and do something based on the results of the features (i.e. show the user a warning or queue something for manual review).
An action is something the user did, or is about to do. For example, some common events include login
, signup
, post_comment
, and add_payment_method
. Actions are just JSON blobs that we call ActionData
. For example, one of the most important actions at Twitter is tweet
:
1 | {"name": "tweet", "username": "floydophone", "text": "hello world!"} |
Save that JSON blob into a file called tweet.json
. We’ll need it later.
Now let’s wire up some basic features in SQRL. Features are similar to variables in other languages. You can bind them using the LET
keyword, and can reference them in any expression. Open up your favorite editor (hint: it’s emacs) and create a file called main.sqrl
with the following code in it:
1 | LET ActionData := input(); |
jsonValue()
is a builtin function that parses a JSON string and returns the value at the given JSONPath. Note that SQRL is smart enough to only parse the JSON once.
Let’s fire up the repl and play with it.
1 | $ npm install -g sqrl-cli |
Cool! You’ll see that the features were extracted successfully, and that there’s a built-in (but overridable) SqrlClock
feature that represents the event time. Note also that features are case sensitive.
The current SQRL implementation runs on Node.js and is a compiler, not an interpreter. As we’ve run this at scale for a long time at a startup that needed to conserve cash, we’ve put in some effort to make this JS as efficient as we could. You can see a readable version of the compiled code for a feature by calling printSource()
:
1 | sqrl> printSource(Username); |
We can also spin up a SQRL service to serve this code over the network:
1 | $ sqrl serve main.sqrl & |
We call this process of unpacking a JSON object into feature names wiring up the action. You usually do this work once per action type. Once an action is wired up, all of your existing features and rules will automatically start to work on the new action.
Let’s say that we want to identify cryptocurrency spam on Twitter. A useful thing to know would be if the user is tweeting about specific cryptocurrencies, like Bitcoin (BTC) or Ethereum (ETH). Let’s create a feature for that in the REPL:
1 | sqrl> LET HasCryptoKeywords := Text CONTAINS "BTC" OR Text CONTAINS "ETH"; |
Now let’s say we want to count how often someone is tweeting about cryptocurrency. We’ll need to use a stateful feature like a counter. Let’s create a new feature NumCryptoTweetsLastDay
:
1 | sqrl> LET NumTweetsAboutCrypto := count(BY Username WHERE HasCryptoKeywords LAST DAY); |
The value is 0
because there are no crypto keywords in our tweet.json
file. Let’s pretend that there are and see what happens:
1 | sqrl> LET HasCryptoKeywords := true; |
Now the value is 1
because the tweet HasCryptoKeywords
. But note that even though we evaluate the feature multiple times, the count doesn’t go up. That’s because SQRL is idempotent. This is a valuable property because it allows us to reprocess actions at a later time if we need to.
The SQRL repl has an EXECUTE
command that will actually update the state and begin the processing of a new action:
1 | sqrl> EXECUTE; |
At this point, we can flip HasCryptoKeywords
back to false, and the counter will cease to increment.
1 | sqrl> LET HasCryptoKeywords := false; |
This is what we mean when we say SQRL is a stateful rules language, and it’s what sets it apart from many other rules languages.
Another thing to note is that the rule writer declaratively specified what they wanted to count. They didn’t explicitly increment or decrement the counter. It’s impossible for the counter to accidentally get out of sync, and the backend can be swapped out for a different data store without changing the SQRL source code. At Smyte, we did this at least four times: we moved from a quick and dirty Redis-based counters implementation, to RocksDB, and finally to Google Cloud BigTable. We also had a separate in-memory implementation for unit tests.
If you wait 15 minutes, this counter will eventually decrement. But why wait? All you need to do is change SqrlClock
to be 15 minutes or more into the future and the counter will drop:
1 | sqrl> LET SqrlClock := "2100-02-13T08:00:00.000Z" |
This is super useful for replaying old actions or writing unit tests.
As we all know, cryptocurrencies come and go… often. We may want to keep a list of known crypto keywords that people can update rather than requiring a code change all the time.
First, make a file CryptoKeywords.txt
containing:
1 | btc |
And then in the repl:
1 | sqrl> LET HasCryptoKeywords := patternMatches("CryptoKeywords.txt", Text); |
Note that patternMatch()
understands word boundaries in multiple languages, so that “hi, seth!” won’t trigger a false positive.
Now let’s say we want to limit people to 100 crypto tweets a day. We’d never write a rule like this in the real world, but it’s a useful example of what’s possible. Rules are just boolean features with some extra metadata and a nice syntax. Here’s how you’d do it.
1 | sqrl> CREATE RULE TooMuchCrypto WHERE NumTweetsAboutCrypto > 10 WITH REASON "User ${Username} tweeted about crypto ${NumTweetsAboutCrypto} in the last day"; |
Your rule should actually do something. Perhaps we want to label every user that is tweeting too much about crypto for manual review. You can do this with a WHEN
block. A WHEN
block ORs together a set of rules, and runs a side effect when one or more of those rules fire.
For example, if you had an addUserToReviewQueue()
function defined, you could call it like this:
1 | WHEN TooMuchCrypto THEN addUserToReviewQueue("cryptospam"); |
The fuction addUserToReviewQueue()
is not included with the default SQRL distribution, but if you have a review queue set up with an API, you can easily define a new when clause function.