npm
, and some features of the application may be unavailable to you.We use Bun for package managing. This experimental approach should come at the added benefit of allowing you to spend less time worrying about packages, and more time coding. Install Bun using
curl -fsSL https://bun.sh/install | bash
Most of the local environment setup right after cloning the repository can be handled with the following commands.
bundle update
bun i
bun -g i firebase-tools
cd ios
pod install
In the future, the application versioning will be rewritten into a github action. As of now, you can update the version (local, and for all platforms) using
bun run bump:<SEMVER_LEVEL>
where <SEMVER_LEVEL>
can be one of the following:
The command creates a new commit in the current branch with the updated version. You can then push to origin these changes as you see fit.
The command should always be ran on the staging branch and from the project root. No version updates should happen on the master branch, nor in smaller branches. This should help keep one source of truth. When merging to the staging branch, never accept changes from the incoming branch.
We use semantic versioning.
Create a local.properties
file in the android
folder and put the following inside:
sdk.dir = /Users/USERNAME/Library/Android/sdk
where you replace USERNAME
with your username. On windows, adjust the path to point to your Android SDK folder.
Make sure to add this path to the ANDROID_HOME
by running
export ANDROID_HOME = /Users/USERNAME/Library/Android/sdk
If this variable is not set, you might be running into the Error: Command failed with EN0ENT bug upon trying to connect to an emulator.
Build the .APK
file using bun run build:android
(calls fastlane android build
). To bundle the build for Google Play release, run fastlane android beta
.
Running bun run build:android
may fail with insufficient permissions to open the gradlew file. In that case, run
chmod +x ./android/gradlew
to make the file readable.
Prettier
shell-format
In most cases, the code written for this repo should be platform-independent. In such cases, each module should have a single file, index.js
, which defines the module's exports. There are, however, some cases in which a feature is intrinsically tied to the underlying platform. In such cases, the following file extensions can be used to export platform-specific code from a module:
index.native.js
index.ios.js
/index.android.js
index.website.js
index.desktop.js
Note that index.js
should be the default and only platform-specific implementations should be done in their respective files. i.e: If you have mobile-specific implementation in index.native.js
, then the desktop/web implementation can be contained in a shared index.js
.
index.ios.js
and index.android.js
are used when the app is running natively on respective platforms. These files are not used when users access the app through mobile browsers, but index.website.js
is used instead. index.native.js
are for both iOS and Android native apps. index.native.js
should not be included in the same module as index.ios.js
or index.android.js
.
Firebase CLI is necessary for any Firebase tests/emulators to run correctly. In order to set up your local environment, run these commands first
bun -g i firebase
firebase login
firebase init
When setting the Firebase rules, here are a several useful behavior patterns to keep in mind:
.read
and .write
rules cascade in the following way:
"higher_node": {
".write": true,
"lower_node": {
".write: false"
}
}
Writing to the higher node: succeeds
Writing to the lower node: succeeds
Explanation: Attempting to write to the lower node will succeed, as a higher node is setting the write
rule to true
. The lower node`s rule is considered, but the higher order rule's allow operation takes precedence. Next, writing to the higher node will succeed too, regardless of the rules in the lower node (these are not considered).
Next, take a look at this example:
"higher_node": {
".write": false,
"lower_node": {
".write: true"
}
}
Writing to the higher node: fails
Writing to the lower node: succeeds
Explanation: When targetting the higher node, the write deny rule means the operation will fail. The rule in the lower node is not taken into consideration (regardless of the outcome). When writing to the lower node, the operation will succeed, as despite the higher node denial, it holds that denial in higher nodes
Under this setup, the database will allow writes into the lower order node even though you are explicitly trying to forbid it by setting the rules to false
there. To forbid writes into the lower order node, make sure to address the higher order rules first.
When dealing with .validate
, the behavior is a little different. The validation rules apply only to the node for which they are written. In other words, they do not cascade. If you, for example, want all members of a node to be either null, or a certain string, you must set this value for all nodes for which this should be relevant. Simply setting this to a higher order node will not suffice.
For read/write, define restrictive rules at higher nodes, and allow broader access at concrete nodes. For example:
"users": {
"$uid": {
".write": "auth != null && $uid ==== auth.uid",
"friend_requests": {
"$friend_request_id": {
".write": "auth != null"
}
}
}
}
friend_requests
node, in which other user will be able to write data too.$uid
node also cascade to the friend_requests
node, protecting the latter from other users making direct modifications to it as a whole. Consequently, the only allowed write operations on this structure for other users will be to append/delete data to the friend_requests
node.For a more fine-grained managemene of the type of data that can be written into a node, use .validate
. In the previous example:
"users": {
"$uid": {
".write": "auth != null && $uid ==== auth.uid",
"friend_requests": {
"$friend_request_id": {
".validate": "(($uid === auth.uid && newData.val() === 'sent') || ($request_id === auth.uid && newData.val() === 'received') || newData.val() === null) && $uid != $request_id",
".write": "auth != null"
}
}
}
}
sent
request to their own node, add received
request to other user's node, or delete a request. They also can not send a friend request to themselves.It might seem tricky to figure out whether to write a rule into .validate
or .read
/.write
. As a rule of thumb, if the rule targets who is requesting the operation, .read
/.write
might be suitable. If specifying the type of data is what you are after, .validate
should do the trick. More than anything, however, keep the cascading nature of each of these rule types in mind, and use it to your advantage to avoid redundancy, while keeping the rules clear and intuitive.
The database migration process is handled in a rather manual fashion. As of now, the procedure is as follows:
_dev
folder contains the folder migrations
. This folder should in turn contain folders input
and output
.input
folder, place the database you want to migrate (do not rename it from the Firebase import)._dev/database/migration-scripts
folder) from the _dev/main.tsx
file. You can run this file using either bun, or ts-node._dev/migrations/output
folder. From there, you can update the relevant database.You can schedule database maintenance directly from the command line using bun run maintenance:schedule
, provided you have the corresponding admin SDK. After the maintenance is over, you will have to cancel it through running bun run maintenance:cancel
. For more detail, see the following sections.
.env
, set the environment of the database you want to schedule the maintenance for..env
file are configured correctly to point to this file.From the project root, run
bun run maintenance:schedule
From there, follow the on-screen instructions.
From the project root, run
bun run maintenance:cancel
and follow the on-screen instructions.
config/maintenance
node of the database. Namely, the maintenance_mode
will be set to true
, while the start and end time will be set to your desired values. As long as the maintenance_mode
flag is on, all users will be shown a maintenance screen upon opening the application.maintenance_mode
to false, allowing application access to users.