Artur Kedzior
Posted on June 13, 2023
I have always look at the simplest way to host my indie projects that is also cheap.
Typically there are two elements to host:
- Database
- Application
If you are build a SaaS and using framework such as React there are actually three elements:
- Database
- API
- Front-end
Going with any of the big cloud providers makes this setup expensive in monthly payments, especially when you are boot strapping indie project now and then.
I found that the fastest & cheapest way to ship things is to use a combination of a cheap VM from Droplet or Hetzner + Firestore with Firebase.
Service | Price | Function |
---|---|---|
Droplet | $6/m | Host React static app & .net API |
Hetzner | $4.8/m | same as above |
Firestore | Free* | NoSQL database |
Firebase | Free* | identity solution |
Firebase & Firestore have a generous free tier which is more than enough to spin up your project fast that will hopefully get successful quickly and it can scale easily.
But what do they do?
Firebase - takes care of your authentication part. You setup the service and they provide you with a npm package to easily integrate your react client code. What does it give you: email + password, gmail, facebook, twitter, github, apple logins out of the box. What you end up storing in your database (user related) is Firebase User ID (UID).
Firestore - is a NoSQL database. Great to start with when you are still shaping your data. It's a good candidate when your data is simple as there is no concept of tables but documents which are basically JSON objects. Given the nature of NoSQL there are some great advantages but also some disadvantages of it. I'm not going to go into details of it here.
Above combo is my favourite when I start with a new indie project. I have basic setup ready within 1 hour.
Let me show here I how set this all up.
Let's start with setting up your .NET API:
Program.cs
services
.AddAuthentication(cfg =>
{
cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = $"https://securetoken.google.com/{firebaseSettings.ProjectId}";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = $"https://securetoken.google.com/{firebaseSettings.ProjectId}",
ValidateAudience = true,
ValidAudience = firebaseSettings.ProjectId,
ValidateLifetime = true
};
});
services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build());
var firebaseJson = JsonSerializer.Serialize(firebaseSettings);
FirebaseApp.Create(new AppOptions
{
Credential = GoogleCredential.FromJson(firebaseJson)
});
// ...
app.UseAuthorization();
app.UseAuthentication();
🔥 That's all you need for your API! 🔥
But wait, what's firebaseSettings ?
Well you need some way to be able to talk to Firebase.
You need to use this
https://www.nuget.org/packages/FirebaseAdmin
That is letting your API to talk to Firebase and add claims to the token issued by Firebase. For example you might want to store thing like user role.
How this can be set up?
When you get to register with Firebase you will have a section where you can download Service Account json file. Just copy values from it to this class. Alternatively you can use appsettings.json
for it.
public class FirebaseSettings
{
public static FirebaseSettings CreateDevelopment() => new
(
"projectId",
"653464563435645",
"-----BEGIN PRIVATE KEY-----\privateKey\n-----END PRIVATE KEY-----\n",
"aaaaaaaaaaaaaa",
"https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-9heco%40GP-72daa.iam.gserviceaccount.com",
"firebase-adminsdk-yourspecificid.iam.gserviceaccount.com"
);
public FirebaseSettings(string projectId, string privateKeyId, string privateKey, string clientId, string clientX509CertUrl, string clientEmail)
{
ProjectId = projectId;
PrivateKeyId = privateKeyId;
PrivateKey = privateKey;
ClientId = clientId;
ClientX509CertUrl = clientX509CertUrl;
ClientEmail = clientEmail;
}
[JsonPropertyName("project_id")]
public string ProjectId { get; init; }
[JsonPropertyName("private_key_id")]
public string PrivateKeyId { get; init; }
[JsonPropertyName("private_key")]
public string PrivateKey { get; init; }
[JsonPropertyName("client_id")]
public string ClientId { get; init; }
[JsonPropertyName("client_x509_cert_url")]
public string ClientX509CertUrl { get; init; }
[JsonPropertyName("client_email")]
public string ClientEmail { get; init; }
[JsonPropertyName("type")]
public string Type => "service_account";
[JsonPropertyName("auth_uri")]
public string AuthUri => "https://accounts.google.com/o/oauth2/auth";
[JsonPropertyName("token_uri")]
public string TokenUri => "https://oauth2.googleapis.com/token";
[JsonPropertyName("auth_provider_x509_cert_url")]
public string AuthProviderx509CertUrl => "https://www.googleapis.com/oauth2/v1/certs";
}
All you need now is to create an endpoint that is authorized and your Firebase user ID would be within the token. Your Firebase user ID can serve as to map with your internal user ID if your really want to.
[Authorize]
public sealed class GetUser : EndpointBaseAsync
.WithoutRequest
.WithActionResult<UserModel>
{
[HttpGet]
public override async Task<ActionResult<UserModel>> HandleAsync(CancellationToken cancellationToken = default)
{
var firebaseUserId = HttpContext.GetFirebaseId()
}
}
public static class HttpContextExtensions
{
public static string GetFirebaseId(this HttpContext context)
=> context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? throw new Exception("Missing name identifier claim on current user");
}
Now the client part!
You will need firebase npm package .
import firebase from 'firebase/compat/app';
import { firebaseConfig } from '../config';
const login = (email, password) => firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(() => {
// After signing with firebase you might want to sign in with your .NET API
axios.post(`${process.env.REACT_APP_API}/users.signin`).then((response)=> {
setProfile(response.data);
});
});
const loginWithGoogle = () => {
const provider = new firebase.auth.GoogleAuthProvider();
return firebase.auth().signInWithPopup(provider);
};
const loginWithFaceBook = () => {
const provider = new firebase.auth.FacebookAuthProvider();
return firebase.auth().signInWithPopup(provider);
};
const loginWithTwitter = () => {
const provider = new firebase.auth.TwitterAuthProvider();
return firebase.auth().signInWithPopup(provider);
};
const register = (email, password, firstName, lastName) =>
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(() => {
// After registering with firebase you might want register using your .NET API
axios.post(`${process.env.REACT_APP_API}/users.signup`, { firstName, lastName, email } )
.then((response) => {
setProfile(response.data);
});
});
const logout = async () => {
await firebase.auth().signOut();
};
// That's the config that is obtained from Firebase Console.
export const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APPID,
measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID
};
🔥 That's all you need for your client code! 🔥
Well not, you need to build your forms and use those methods. If you want to skip it all along you can also use FirebaseUI which provides you with already made login form.
Drop me any comment if you get stuck or there is something unclear.
Good luck and ....
Made The Code Be with You!
Posted on June 13, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.